[
  {
    "path": ".babelrc",
    "content": "{\n  \"comments\": true,\n  \"plugins\": [\n    \"lodash\",\n    \"graphql-tag\",\n    \"@babel/plugin-syntax-dynamic-import\",\n    \"@babel/plugin-syntax-import-meta\",\n    \"@babel/plugin-proposal-class-properties\",\n    \"@babel/plugin-proposal-json-strings\",\n    [\n      \"@babel/plugin-proposal-decorators\",\n      {\n        \"legacy\": true\n      }\n    ],\n    \"@babel/plugin-proposal-function-sent\",\n    \"@babel/plugin-proposal-export-namespace-from\",\n    \"@babel/plugin-proposal-numeric-separator\",\n    \"@babel/plugin-proposal-throw-expressions\",\n    [\n      \"prismjs\", {\n        \"languages\": [\"clike\", \"markup\"],\n        \"plugins\": [\"line-numbers\", \"autoloader\", \"normalize-whitespace\", \"copy-to-clipboard\", \"toolbar\"],\n        \"theme\": \"twilight\",\n        \"css\": true\n      }\n    ]\n  ],\n  \"presets\": [\n    [\n      \"@babel/preset-env\", {\n        \"useBuiltIns\": \"entry\",\n        \"corejs\": 3,\n        \"debug\": false\n      }\n    ]\n  ]\n}\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// How to get remote container development working with VSCode:\n// 1. Install \"Remote Development\" extension pack (ms-vscode-remote.vscode-remote-extensionpack)\n// 2. Select \"Remote Containers - Reopen in container\"\n\n{\n  \"name\": \"Wiki.js\",\n  \"dockerComposeFile\": [\n    \"../dev/containers/docker-compose.yml\"\n  ],\n  \"forwardPorts\": [3000, 3001],\n  \"service\": \"wiki\",\n  \"workspaceFolder\": \"/wiki\",\n  \"settings\": {\n    \"terminal.integrated.shell.linux\": \"/bin/bash\"\n  },\n  \"extensions\": [\n\t\"EditorConfig.editorconfig\",\n\t\"dbaeumer.vscode-eslint\",\n\t\"christian-kohler.path-intellisense\",\n\t\"mrmlnc.vscode-puglint\",\n\t\"octref.vetur\",\n\t\"dzannotti.vscode-babel-coloring\",\n\t\"wayou.vscode-todo-highlight\",\n\t\"visualstudioexptteam.vscodeintellicode\",\n\t\"lukas-tr.materialdesignicons-intellisense\",\n\t\"codezombiech.gitignore\",\n\t\"kumar-harsh.graphql-for-vscode\",\n\t\"mrmlnc.vscode-duplicate\",\n\t\"oderwat.indent-rainbow\",\n\t\"christian-kohler.npm-intellisense\"\n],\n  \"postCreateCommand\": [\"yarn\", \"install\"]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\nend_of_line = lf\ninsert_final_newline = true\n\n[*.{jade,pug,md}]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab\nindent_size = 4\n"
  },
  {
    "path": ".eslintignore",
    "content": "**/node_modules/**\n**/*.min.js\nassets/**\nclient/libs/**\ncoverage/**\nrepo/**\ndata/**\nlogs/**\n"
  },
  {
    "path": ".eslintrc.yml",
    "content": "extends:\n  - requarks\n  - plugin:vue/strongly-recommended\n  - plugin:cypress/recommended\nenv:\n  node: true\n  jest: true\nparserOptions:\n  parser: babel-eslint\n  ecmaVersion: 2017\n  allowImportExportEverywhere: true\nglobals:\n  document: false\n  navigator: false\n  window: false\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Common settings that generally should always be used with your language specific settings\n\n# Auto detect text files and perform LF normalization\n# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/\n*          text=auto\n\n#\n# The above will handle all files NOT found below\n#\n\n# Documents\n*.bibtex   text diff=bibtex\n*.doc\t        diff=astextplain\n*.DOC\t        diff=astextplain\n*.docx          diff=astextplain\n*.DOCX          diff=astextplain\n*.dot           diff=astextplain\n*.DOT           diff=astextplain\n*.pdf           diff=astextplain\n*.PDF           diff=astextplain\n*.rtf           diff=astextplain\n*.RTF\t        diff=astextplain\n*.md       text\n*.tex      text diff=tex\n*.adoc     text\n*.textile  text\n*.mustache text\n*.csv      text\n*.tab      text\n*.tsv      text\n*.txt      text\n*.sql      text\n\n# Graphics\n*.png      binary\n*.jpg      binary\n*.jpeg     binary\n*.gif      binary\n*.tif      binary\n*.tiff     binary\n*.ico      binary\n# SVG treated as an asset (binary) by default.\n*.svg      text\n# If you want to treat it as binary,\n# use the following line instead.\n# *.svg    binary\n*.eps      binary\n\n# Scripts\n*.bash     text eol=lf\n*.sh       text eol=lf\n# These are explicitly windows files and should use crlf\n*.bat      text eol=crlf\n*.cmd      text eol=crlf\n*.ps1      text eol=crlf\n\n# Serialisation\n*.json     text\n*.toml     text\n*.xml      text\n*.yaml     text\n*.yml      text\n\n# Archives\n*.7z       binary\n*.gz       binary\n*.tar      binary\n*.zip      binary\n\n#\n# Exclude files from exporting\n#\n\n.gitattributes export-ignore\n.gitignore     export-ignore\n\n# Auto detect text files and perform LF normalization\n# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/\n*    text=auto\n\n*.cs text diff=csharp\n\n# Treat all Go files in this repo as binary, with no git magic updating\n# line endings. Windows users contributing to Go will need to use a\n# modern version of git and editors capable of LF line endings.\n\n*.go -text diff=golang\n\n## GITATTRIBUTES FOR WEB PROJECTS\n#\n# These settings are for any web project.\n#\n# Details per file setting:\n#   text    These files should be normalized (i.e. convert CRLF to LF).\n#   binary  These files are binary and should be left untouched.\n#\n# Note that binary is a macro for -text -diff.\n######################################################################\n\n# Auto detect\n##   Handle line endings automatically for files detected as\n##   text and leave all files detected as binary untouched.\n##   This will handle all files NOT defined below.\n*                 text=auto\n\n# Source code\n*.bash            text eol=lf\n*.bat             text eol=crlf\n*.cmd             text eol=crlf\n*.coffee          text\n*.css             text\n*.htm             text diff=html\n*.html            text diff=html\n*.inc             text\n*.ini             text\n*.js              text\n*.json            text\n*.jsx             text\n*.less            text\n*.ls              text\n*.map             text -diff\n*.od              text\n*.onlydata        text\n*.php             text diff=php\n*.pl              text\n*.ps1             text eol=crlf\n*.py              text diff=python\n*.rb              text diff=ruby\n*.sass            text\n*.scm             text\n*.scss            text diff=css\n*.sh              text eol=lf\n*.sql             text\n*.styl            text\n*.tag             text\n*.ts              text\n*.tsx             text\n*.xml             text\n*.xhtml           text diff=html\n\n# Docker\n*.dockerignore    text\nDockerfile        text\n\n# Documentation\n*.ipynb           text\n*.markdown        text\n*.md              text\n*.mdwn            text\n*.mdown           text\n*.mkd             text\n*.mkdn            text\n*.mdtxt           text\n*.mdtext          text\n*.txt             text\nAUTHORS           text\nCHANGELOG         text\nCHANGES           text\nCONTRIBUTING      text\nCOPYING           text\ncopyright         text\n*COPYRIGHT*       text\nINSTALL           text\nlicense           text\nLICENSE           text\nNEWS              text\nreadme            text\n*README*          text\nTODO              text\n\n# Templates\n*.dot             text\n*.ejs             text\n*.haml            text\n*.handlebars      text\n*.hbs             text\n*.hbt             text\n*.jade            text\n*.latte           text\n*.mustache        text\n*.njk             text\n*.phtml           text\n*.tmpl            text\n*.tpl             text\n*.twig            text\n*.vue             text\n\n# Linters\n.csslintrc        text\n.eslintrc         text\n.htmlhintrc       text\n.jscsrc           text\n.jshintrc         text\n.jshintignore     text\n.stylelintrc      text\n\n# Configs\n*.bowerrc         text\n*.cnf             text\n*.conf            text\n*.config          text\n.babelrc          text\n.browserslistrc   text\n.editorconfig     text\n.env              text\n.gitattributes    text\n.gitconfig        text\n.htaccess         text\n*.lock            text -diff\npackage-lock.json text -diff\n*.npmignore       text\n*.yaml            text\n*.yml             text\nbrowserslist      text\nMakefile          text\nmakefile          text\n\n# Heroku\nProcfile          text\n.slugignore       text\n\n# Graphics\n*.ai              binary\n*.bmp             binary\n*.eps             binary\n*.gif             binary\n*.gifv            binary\n*.ico             binary\n*.jng             binary\n*.jp2             binary\n*.jpg             binary\n*.jpeg            binary\n*.jpx             binary\n*.jxr             binary\n*.pdf             binary\n*.png             binary\n*.psb             binary\n*.psd             binary\n# SVG treated as an asset (binary) by default.\n*.svg             text\n# If you want to treat it as binary,\n# use the following line instead.\n# *.svg           binary\n*.svgz            binary\n*.tif             binary\n*.tiff            binary\n*.wbmp            binary\n*.webp            binary\n\n# Audio\n*.kar             binary\n*.m4a             binary\n*.mid             binary\n*.midi            binary\n*.mp3             binary\n*.ogg             binary\n*.ra              binary\n\n# Video\n*.3gpp            binary\n*.3gp             binary\n*.as              binary\n*.asf             binary\n*.asx             binary\n*.fla             binary\n*.flv             binary\n*.m4v             binary\n*.mng             binary\n*.mov             binary\n*.mp4             binary\n*.mpeg            binary\n*.mpg             binary\n*.ogv             binary\n*.swc             binary\n*.swf             binary\n*.webm            binary\n\n# Archives\n*.7z              binary\n*.gz              binary\n*.jar             binary\n*.rar             binary\n*.tar             binary\n*.zip             binary\n\n# Fonts\n*.ttf             binary\n*.eot             binary\n*.otf             binary\n*.woff            binary\n*.woff2           binary\n\n# Executables\n*.exe             binary\n*.pyc             binary\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn 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.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject 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.\n\nProject 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.\n\n## Scope\n\nThis 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.\n\n## Enforcement\n\nInstances 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.\n\nProject 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.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contribute\n\n## Introduction\n\nFirst, thank you for considering contributing to Wiki.js! It's people like you that make the open source community such a great community! 😊\n\nWe welcome any type of contribution, not only code. You can help with\n- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)\n- **Marketing**: writing blog posts, howto's, printing stickers, ...\n- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...\n- **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.\n- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wikijs).\n\n## Your First Contribution\n\nWorking 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).\n\n## Submitting code\n\nAny 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.\n\n## Code review process\n\nThe 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.\nIt is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?\n\n## Requesting new features / enhancements\n\nUse the feature request board to submit new ideas and vote on which ideas should be integrated first.\n\n:triangular_flag_on_post: [https://js.wiki/feedback/](https://js.wiki/feedback/)\n\n*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*.\n\n## Financial contributions\n\nWe also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wikijs).\nAnyone 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.\n\n## Questions\n\nIf 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!).\nYou can also reach us at <hello@wikijs.opencollective.com>.\n\n## Credits\n\n### Contributors\n\nThank you to all the people who have already contributed to Wiki.js!\n<a href=\"https://github.com/Requarks/wiki/graphs/contributors\"><img src=\"https://opencollective.com/wikijs/contributors.svg?width=890\" /></a>\n\n\n### Backers\n\nThank you to all our backers! [[Become a backer](https://opencollective.com/wikijs#backer)]\n\n<a href=\"https://opencollective.com/wikijs#backers\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/backers.svg?width=890\"></a>\n\n\n### Sponsors\n\nThank 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))\n\n<a href=\"https://opencollective.com/wikijs/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wikijs/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/9/avatar.svg\"></a>\n\n<!-- This `CONTRIBUTING.md` is based on @nayafia's template https://github.com/nayafia/contributing-template -->\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [NGPixel]\npatreon: requarks\nopen_collective: wikijs\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncustom: # Replace with a single custom sponsorship URL\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Help / Questions\n    url: https://github.com/Requarks/wiki/discussions/categories/help-questions\n    about: Ask the community for help on using or setting up Wiki.js\n  - name: Errors / Bug Reports\n    url: https://github.com/Requarks/wiki/discussions/categories/error-bug-report\n    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.\n  - name: Request a new feature / improvement\n    url: https://feedback.js.wiki/wiki\n    about: Submit ideas for new features or improvements.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": ""
  },
  {
    "path": ".github/auto_assign.yml",
    "content": "# Set to true to add reviewers to pull requests\naddReviewers: true\n\n# Set to true to add assignees to pull requests\naddAssignees: true\n\n# A list of reviewers to be added to pull requests (GitHub user name)\nreviewers: \n  - NGPixel\n\n# A list of keywords to be skipped the process that add reviewers if pull requests include it \nskipKeywords:\n  - wip\n\n# A number of reviewers added to the pull request\n# Set 0 to add all the reviewers (default: 0)\nnumberOfReviewers: 0\n"
  },
  {
    "path": ".github/issuecomplete.yml",
    "content": "# The name of the label to apply when an issue does not have all tasks checked\nlabelName: invalid\n\n# The color of the label in hex format (without #)\nlabelColor:\n\n# The text of the comment to add to the issue in addition to the label\ncommentText: >\n  You haven't provided the required info about your host! (OS, Wiki.js version, Database engine)\n\n# Whether or not to ensure all checkboxes are checked\ncheckCheckboxes: false\n\n# Keywords to look for in the body of the issue\nkeywords:\n  - Wiki.js version\n  - Database engine\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build + Publish\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - 'v*'\n\nenv:\n  BASE_DEV_VERSION: 2.5.0\n\njobs:\n\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set Build Variables\n      run: |\n        if [[ \"$GITHUB_REF\" =~ ^refs/tags/v* ]]; then\n          echo \"Using TAG mode: $GITHUB_REF_NAME\"\n          echo \"REL_VERSION=$GITHUB_REF_NAME\" >> $GITHUB_ENV\n          echo \"REL_VERSION_STRICT=${GITHUB_REF_NAME#?}\" >> $GITHUB_ENV\n        else\n          echo \"Using BRANCH mode: v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\"\n          echo \"REL_VERSION=v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\" >> $GITHUB_ENV\n          echo \"REL_VERSION_STRICT=$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\" >> $GITHUB_ENV\n        fi\n\n    - name: Disable DEV Flag + Set Version\n      run: |\n        sudo apt-get install jq -y\n        mv package.json pkg-temp.json\n        jq --arg vs \"$REL_VERSION_STRICT\" -r '. + {dev:false, version:$vs}' pkg-temp.json > package.json\n        rm pkg-temp.json\n        cat package.json\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.repository_owner }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Build and push Docker images\n      uses: docker/build-push-action@v5\n      with:\n        context: .\n        file: dev/build/Dockerfile\n        push: true\n        tags: |\n          requarks/wiki:canary\n          requarks/wiki:canary-${{ env.REL_VERSION_STRICT }}\n          ghcr.io/requarks/wiki:canary\n          ghcr.io/requarks/wiki:canary-${{ env.REL_VERSION_STRICT }}\n\n    - name: Extract compiled files\n      run: |\n        mkdir -p _dist\n        docker create --name wiki ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT\n        docker cp wiki:/wiki _dist\n        docker rm wiki\n        rm _dist/wiki/config.yml\n        cp ./config.sample.yml _dist/wiki/config.sample.yml\n        find _dist/wiki/ -printf \"%P\\n\" | tar -czf wiki-js.tar.gz --no-recursion -C _dist/wiki/ -T -\n\n    - name: Upload a Build Artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: drop\n        path: wiki-js.tar.gz\n\n  cypress:\n    name: Run Cypress Tests\n    runs-on: ubuntu-latest\n    needs: [build]\n\n    strategy:\n      matrix:\n        dbtype: [postgres, mysql, mariadb, sqlite]\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set Test Variables\n      run: |\n        if [[ \"$GITHUB_REF\" =~ ^refs/tags/v* ]]; then\n          echo \"Using TAG mode: $GITHUB_REF_NAME\"\n          echo \"REL_VERSION_STRICT=${GITHUB_REF_NAME#?}\" >> $GITHUB_ENV\n        else\n          echo \"Using BRANCH mode: v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\"\n          echo \"REL_VERSION_STRICT=$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\" >> $GITHUB_ENV\n        fi\n\n    - name: Run Tests\n      env:\n        MATRIXENV: ${{ matrix.dbtype }}\n        CYPRESS_KEY: ${{ secrets.CYPRESS_KEY }}\n      run: |\n        chmod u+x dev/cypress/ci-setup.sh\n        dev/cypress/ci-setup.sh\n        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\n\n  arm:\n    name: ARM Build\n    runs-on: ubuntu-latest\n    needs: [cypress]\n    permissions:\n      packages: write\n\n    strategy:\n      matrix:\n        include:\n          - platform: linux/arm64\n            docker: arm64\n          # - platform: linux/arm/v7\n          #   docker: armv7\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set Version Variables\n      run: |\n        if [[ \"$GITHUB_REF\" =~ ^refs/tags/v* ]]; then\n          echo \"Using TAG mode: $GITHUB_REF_NAME\"\n          echo \"REL_VERSION_STRICT=${GITHUB_REF_NAME#?}\" >> $GITHUB_ENV\n        else\n          echo \"Using BRANCH mode: v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\"\n          echo \"REL_VERSION_STRICT=$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER\" >> $GITHUB_ENV\n        fi\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v3\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v3\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.repository_owner }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Download a Build Artifact\n      uses: actions/download-artifact@v4\n      with:\n        name: drop\n        path: drop\n\n    - name: Extract Build\n      run: |\n        mkdir -p build\n        tar -xzf $GITHUB_WORKSPACE/drop/wiki-js.tar.gz -C $GITHUB_WORKSPACE/build --exclude=node_modules\n\n    - name: Build and push Docker images\n      uses: docker/build-push-action@v5\n      with:\n        context: .\n        file: dev/build-arm/Dockerfile\n        platforms: ${{ matrix.platform }}\n        provenance: false\n        push: true\n        tags: |\n          requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }}\n          ghcr.io/requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }}\n\n  windows:\n    name: Windows Build\n    runs-on: windows-latest\n    needs: [cypress]\n\n    steps:\n    - name: Setup Node.js environment\n      uses: actions/setup-node@v4\n      with:\n        node-version: 20.x\n\n    - name: Download a Build Artifact\n      uses: actions/download-artifact@v4\n      with:\n        name: drop\n        path: drop\n\n    - name: Extract Build\n      run: |\n        mkdir -p win\n        tar -xzf $env:GITHUB_WORKSPACE\\drop\\wiki-js.tar.gz -C $env:GITHUB_WORKSPACE\\win\n        Copy-Item win\\node_modules\\extract-files\\package.json patch-extractfile.json -Force\n        Remove-Item -Path win\\node_modules -Force -Recurse\n\n    - name: Install Dependencies\n      run: |\n        yarn --production --frozen-lockfile --non-interactive\n        yarn patch-package\n      working-directory: win\n\n    - name: Fix patched packages\n      run: |\n        Copy-Item patch-extractfile.json win\\node_modules\\extract-files\\package.json -Force\n\n    - name: Create Bundle\n      run: tar -czf wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\\win .\n\n    - name: Upload a Build Artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: drop-win\n        path: wiki-js-windows.tar.gz\n\n  beta:\n    name: Publish Beta Images\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/v')\n    needs: [build, arm, windows]\n    permissions:\n      packages: write\n\n    steps:\n    - name: Set Version Variables\n      run: |\n        echo \"Using TAG mode: $GITHUB_REF_NAME\"\n        echo \"REL_VERSION_STRICT=${GITHUB_REF_NAME#?}\" >> $GITHUB_ENV\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.repository_owner }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Create and Push Manifests\n      run: |\n        echo \"Creating the manifests...\"\n\n        docker manifest create requarks/wiki:beta-$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT\n        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\n\n        echo \"Pushing the manifests...\"\n\n        docker manifest push -p requarks/wiki:beta-$REL_VERSION_STRICT\n        docker manifest push -p ghcr.io/requarks/wiki:beta-$REL_VERSION_STRICT\n\n  release:\n    name: Publish Release Images\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/v')\n    environment: prod\n    needs: [beta]\n    permissions:\n      packages: write\n      contents: write\n\n    steps:\n    - name: Set Version Variables\n      run: |\n        echo \"Using TAG mode: $GITHUB_REF_NAME\"\n        echo \"REL_VERSION_STRICT=${GITHUB_REF_NAME#?}\" >> $GITHUB_ENV\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.repository_owner }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Create and Push Manifests\n      run: |\n        echo \"Fetching semver tool...\"\n        curl -LJO https://static.requarks.io/semver\n        chmod +x semver\n\n        MAJOR=`./semver get major $REL_VERSION_STRICT`\n        MINOR=`./semver get minor $REL_VERSION_STRICT`\n        MAJORMINOR=\"$MAJOR.$MINOR\"\n\n        echo \"Using major $MAJOR and minor $MINOR...\"\n        echo \"Creating the manifests...\"\n\n        docker manifest create requarks/wiki:$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT\n        docker manifest create requarks/wiki:$MAJOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT\n        docker manifest create requarks/wiki:$MAJORMINOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT\n        docker manifest create requarks/wiki:latest requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT\n        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\n        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\n        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\n        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\n\n        echo \"Pushing the manifests...\"\n\n        docker manifest push -p requarks/wiki:$REL_VERSION_STRICT\n        docker manifest push -p requarks/wiki:$MAJOR\n        docker manifest push -p requarks/wiki:$MAJORMINOR\n        docker manifest push -p requarks/wiki:latest\n        docker manifest push -p ghcr.io/requarks/wiki:$REL_VERSION_STRICT\n        docker manifest push -p ghcr.io/requarks/wiki:$MAJOR\n        docker manifest push -p ghcr.io/requarks/wiki:$MAJORMINOR\n        docker manifest push -p ghcr.io/requarks/wiki:latest\n\n    - name: Download Linux Build\n      uses: actions/download-artifact@v4\n      with:\n        name: drop\n        path: drop\n\n    - name: Download Windows Build\n      uses: actions/download-artifact@v4\n      with:\n        name: drop-win\n        path: drop-win\n\n    - name: Generate Changelog\n      id: changelog\n      uses: Requarks/changelog-action@v1\n      with:\n        token: ${{ github.token }}\n        tag: ${{ github.ref_name }}\n        writeToFile: false\n\n    - name: Update GitHub Release\n      uses: ncipollo/release-action@v1.12.0\n      with:\n        allowUpdates: true\n        draft: false\n        makeLatest: true\n        name: ${{ github.ref_name }}\n        body: ${{ steps.changelog.outputs.changes }}\n        token: ${{ github.token }}\n        artifacts: 'drop/wiki-js.tar.gz,drop-win/wiki-js-windows.tar.gz'\n\n    # - name: Notify Slack Releases Channel\n    #   uses: slackapi/slack-github-action@v1.26.0\n    #   with:\n    #     payload: |\n    #       {\n    #         \"text\": \"Wiki.js ${{ github.ref_name }} has been released.\"\n    #       }\n    #   env:\n    #     SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}\n    #     SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK\n\n    - name: Notify Telegram Channel\n      uses: appleboy/telegram-action@v0.1.1\n      with:\n        to: ${{ secrets.TELEGRAM_TO }}\n        token: ${{ secrets.TELEGRAM_TOKEN }}\n        format: markdown\n        disable_web_page_preview: true\n        message: |\n          Wiki.js *${{ github.ref_name }}* has been released!\n          See [release notes](https://github.com/requarks/wiki/releases) for details.\n\n    - name: Notify Discord Channel\n      uses: sebastianpopp/discord-action@v2.0\n      with:\n        webhook: ${{ secrets.DISCORD_WEBHOOK }}\n        message: Wiki.js ${{ github.ref_name }} has been released! See https://github.com/requarks/wiki/releases for details.\n\n  # build-do-image:\n  #   name: Build DigitalOcean Image\n  #   runs-on: ubuntu-latest\n  #   needs: [release]\n\n  #   steps:\n  #   - uses: actions/checkout@v4\n\n  #   - name: Set Version Variables\n  #     run: |\n  #       echo \"Using TAG mode: $GITHUB_REF_NAME\"\n  #       echo \"REL_VERSION_STRICT=${GITHUB_REF_NAME#?}\" >> $GITHUB_ENV\n\n  #   - name: Install Packer\n  #     run: |\n  #       curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -\n  #       sudo apt-add-repository \"deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main\"\n  #       sudo apt-get update && sudo apt-get install packer\n\n  #   - name: Build Droplet Image\n  #     env:\n  #       DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}\n  #       WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }}\n  #     working-directory: dev/packer\n  #     run: |\n  #       packer build digitalocean.json\n"
  },
  {
    "path": ".github/workflows/helm.yml",
    "content": "name: Helm Chart CI\n\non:\n  # Triggers the workflow on push or pull request events but only for the dev branch\n  push:\n    branches: [ main ]\n    paths: [ dev/helm/** ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  build:\n    name: Update Chart\n    runs-on: ubuntu-latest\n\n    steps:\n      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it\n      - uses: actions/checkout@v6\n      \n      - name: Package and Push Chart\n        run: |\n          export CHARTVER=$(yq '.version' dev/helm/Chart.yaml)\n          helm plugin install https://github.com/chartmuseum/helm-push.git\n          helm repo add chartmuseum https://charts.js.wiki\n          helm cm-push --version=\"$CHARTVER\" --username=\"${{secrets.HELM_REPO_USERNAME}}\" --password=\"${{secrets.HELM_REPO_PASSWORD}}\" dev/helm/ chartmuseum\n          helm repo remove chartmuseum\n"
  },
  {
    "path": ".github/workflows/packer.yml",
    "content": "name: Build DigitalOcean Image\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'App Version'\n        required: true\n        type: string\n\njobs:\n  build-do-image:\n    name: Build DigitalOcean Image\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - name: Install Packer\n      run: |\n        wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg\n        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\n        sudo apt update && sudo apt install packer\n\n    - name: Build Droplet Image\n      env:\n        DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}\n        WIKI_APP_VERSION: ${{ github.event.inputs.version }}\n      working-directory: dev/packer\n      run: |\n        packer plugins install github.com/digitalocean/digitalocean\n        packer build digitalocean.json\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\n/logs\n\n# Deployment builds\ndist\n\n# Dependency directories\nnode_modules\nnpm/node_modules\n\n# NPM / Yarn\n.npm\n.node_repl_history\nnpm-debug.log*\n.yarn\n\n# Generated assets\n/assets\nserver/views/master.pug\nserver/views/legacy/master.pug\nserver/views/setup.pug\n\n# Webpack\n.webpack-cache\n.fusebox\n\n# Config Files\n/config.yml\n\n# Data directories\n/repo\n/data\n/uploads\n/content\n/temp\n*.sqlite\n\n# IDE exclude\n.idea\n*.sublime-*\n\n# Test results\ntest-results/\n.scannerwork\n\n# Localization Resources\n/server/locales/**/*.yml\n"
  },
  {
    "path": ".npmrc",
    "content": "save-exact = true\nsave-prefix = \"\"\n"
  },
  {
    "path": ".nvmrc",
    "content": "v24.12.0\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"EditorConfig.editorconfig\",\n    \"dbaeumer.vscode-eslint\",\n    \"christian-kohler.path-intellisense\",\n    \"mrmlnc.vscode-puglint\",\n    \"octref.vetur\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible Node.js debug attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n    {\n      \"type\": \"node\",\n      \"request\": \"attach\",\n      \"name\": \"Attach (Inspector Protocol)\",\n      \"port\": 9229,\n      \"protocol\": \"inspector\"\n    },\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"program\": \"${workspaceRoot}\\\\server.js\"\n        },\n        {\n            \"type\": \"node\",\n            \"request\": \"attach\",\n            \"name\": \"Attach to Port\",\n            \"address\": \"localhost\",\n            \"port\": 9222\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.enable\": true,\n  \"puglint.enable\": true,\n  \"editor.formatOnSave\": false,\n  \"editor.tabSize\": 2,\n  \"eslint.validate\": [\n    \"javascript\",\n    \"vue\"\n  ],\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  },\n  \"i18n-ally.localesPaths\": [\n    \"server/locales\"\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<http://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://static.requarks.io/logo/wikijs-full-darktheme.svg\">\n  <img alt=\"Wiki.js\" src=\"https://static.requarks.io/logo/wikijs-full.svg\" width=\"600\">\n</picture>\n\n[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases)\n[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE)\n[![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat&logo=javascript&logoColor=white)](http://standardjs.com/)\n[![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml)  \n[![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)\n[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)\n[![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases)\n[![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/)  \n[![Chat on Discord](https://img.shields.io/badge/discord-join-8D96F6.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/rcxt9QS2jd)\n[![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)\n[![Follow on Telegram](https://img.shields.io/badge/telegram-%40wiki__js-blue.svg?style=flat&logo=telegram)](https://t.me/wiki_js)\n[![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/)\n\n##### A modern, lightweight and powerful wiki app built on NodeJS\n\n</div>\n\n- **[Official Website](https://js.wiki/)**\n- **[Documentation](https://docs.requarks.io/)**\n- [Requirements](https://docs.requarks.io/install/requirements)\n- [Installation](https://docs.requarks.io/install)\n- [Demo](https://docs.requarks.io/demo)\n- [Changelog](https://github.com/requarks/wiki/releases)\n- [Feature Requests](https://feedback.js.wiki/wiki)\n- Chat with us on [Discord](https://discord.gg/rcxt9QS2jd)\n- [Translations](https://docs.requarks.io/dev/translations) *(We need your help!)*\n- [E2E Testing Results](https://dashboard.cypress.io/projects/r7qxah/runs)\n- [Special Thanks](#special-thanks)\n- [Contribute](#contributors)\n\n[Follow our Twitter feed](https://twitter.com/requarks) to learn about upcoming updates and new releases!\n\n<h2 align=\"center\">Donate</h2>\n\n<div align=\"center\">\n\nWiki.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`).\n  \n  [![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship)\n  [![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks)\n  [![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)\n  [![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)  \n  [![Donate via Ethereum](https://img.shields.io/badge/donate-ethereum-999.svg?style=popout&logo=ethereum&logoColor=CCC)](https://etherscan.io/address/0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5)\n  [![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)\n  [![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)\n\n</div>\n\n<h2 align=\"center\">Gold Tier Sponsors</h2>\n\n<div align=\"center\">\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\" width=\"444\">\n        <a href=\"https://trans-zero.com/\" target=\"_blank\">\n          <img src=\"https://cdn.js.wiki/images/sponsors/transzero.png\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n</div>\n\n<h2 align=\"center\">GitHub Sponsors</h2>\n\nSupport 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)]\n\n<div align=\"center\">\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\" width=\"444\">\n        <a href=\"https://www.stellarhosted.com/\" target=\"_blank\">\n          <img src=\"https://cdn.js.wiki/images/sponsors/stellarhosted.png\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n</div>\n\n<div align=\"center\">\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://acceleanation.com/\" target=\"_blank\">\n          <img src=\"https://avatars.githubusercontent.com/u/41210718?s=200&v=4\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/alexksso\" target=\"_blank\">\n          Alexander Casassovici<br />(@alexksso)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/broxen\" target=\"_blank\">\n          Broxen<br />(@broxen)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/xDacon\" target=\"_blank\">\n          Dacon<br />(@xDacon)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/DonNabla\" target=\"_blank\">\n          Maxime Pierre<br />(@DonNabla)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/GigabiteLabs\" target=\"_blank\">\n          <img src=\"https://static.requarks.io/sponsors/gigabitelabs-148x129.png\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://www.hostwiki.com/\" target=\"_blank\">\n          <img src=\"https://cdn.js.wiki/images/sponsors/hostwiki.png\">\n        </a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/JayDaley\" target=\"_blank\">\n          Jay Daley<br />(@JayDaley)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/idokka\" target=\"_blank\">\n          Oleksii<br />(@idokka)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://www.openhost-network.com/\" target=\"_blank\">\n          <img src=\"https://avatars.githubusercontent.com/u/114218287?s=200&v=4\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://www.prevo.ch/\" target=\"_blank\">\n          <img src=\"https://avatars.githubusercontent.com/u/114394792?v=4\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"https://github.com/shanekearney\" target=\"_blank\">\n          Shane Kearney<br />(@shanekearney)\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\">\n        <a href=\"http://www.taicep.org/\" target=\"_blank\">\n          <img src=\"https://avatars.githubusercontent.com/u/160072306?v=4\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\" width=\"130\"></td>\n    </tr>\n  </tbody>\n</table>\n\n<table><tbody><tr><td>\n<img width=\"441\" height=\"1\" />\n\n- Akira Suenami ([@a-suenami](https://github.com/a-suenami))\n- Armin Reiter ([@arminreiter](https://github.com/arminreiter))\n- Arnaud Marchand ([@snuids](https://github.com/snuids))\n- Brian Douglass ([@bhdouglass](https://github.com/bhdouglass))\n- Bryon Vandiver ([@asterick](https://github.com/asterick))\n- Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer))\n- Charlie Schliesser ([@charlie-s](https://github.com/charlie-s))\n- Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC))\n- Cole Manning ([@RVRX](https://github.com/RVRX))\n- CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin))\n- Daniel Horner ([@danhorner](https://github.com/danhorner))\n- David Christian Holin ([@SirGibihm](https://github.com/SirGibihm))\n- Dragan Espenschied ([@despens](https://github.com/despens))\n- Elijah Zobenko ([@he110](https://github.com/he110))\n- Emerson-Perna ([@Emerson-Perna](https://github.com/Emerson-Perna))\n- Ernie ([@iamernie](https://github.com/iamernie))\n- Fabio Ferrari ([@devxops](https://github.com/devxops))\n- Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa))\n- Florian Moss ([@florianmoss](https://github.com/florianmoss))\n- GoodCorporateCitizen ([@GoodCorporateCitizen](https://github.com/GoodCorporateCitizen))\n- HeavenBay ([@HeavenBay](https://github.com/heavenbay))\n- HikaruEgashira ([@HikaruEgashira](https://github.com/HikaruEgashira))\n- Ian Hyzy ([@ianhyzy](https://github.com/ianhyzy))\n- Jaimyn Mayer ([@jabelone](https://github.com/jabelone))\n- Jay Lee ([@polyglotm](https://github.com/polyglotm))\n- Kelly Wardrop ([@dropcoded](https://github.com/dropcoded))\n- Loki ([@binaryloki](https://github.com/binaryloki))\n- MaFarine ([@MaFarine](https://github.com/MaFarine))\n- Marcilio Leite Neto ([@marclneto](https://github.com/marclneto))\n- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))\n- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))\n- Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA))\n- Mitchell Rowton ([@mrowton](https://github.com/mrowton))\n        \n</td><td>\n<img width=\"441\" height=\"1\" />\n\n- M. Scott Ford ([@mscottford](https://github.com/mscottford))\n- Nick Halase ([@nhalase](https://github.com/nhalase))\n- Nick Price ([@DominoTree](https://github.com/DominoTree))\n- Nina Reynolds ([@cutecycle](https://github.com/cutecycle))\n- Noel Cower ([@nilium](https://github.com/nilium))\n- Oleksandr Koltsov ([@crambo](https://github.com/crambo))\n- Phi Zeroth ([@phizeroth](https://github.com/phizeroth))\n- Philipp Schmitt ([@pschmitt](https://github.com/pschmitt))\n- Robert Lanzke ([@winkelement](https://github.com/winkelement))\n- Ruizhe Li ([@liruizhe1995](https://github.com/liruizhe1995))\n- Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth))\n- Sean Coffey ([@seanecoffey](https://github.com/seanecoffey))\n- Simon Ott ([@ottsimon](https://github.com/ottsimon))\n- Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro))\n- Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu))\n- Tim Elmer ([@tim-elmer](https://github.com/tim-elmer))\n- Tyler Denman ([@tylerguy](https://github.com/tylerguy))\n- Victor Bilgin ([@vbilgin](https://github.com/vbilgin))\n- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions))\n- YazMogg35 ([@YazMogg35](https://github.com/YazMogg35))\n- Yu Yongwoo ([@uyu423](https://github.com/uyu423))\n- ameyrakheja ([@ameyrakheja](https://github.com/ameyrakheja))\n- aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))\n- aytaa ([@aytaa](https://github.com/aytaa))\n- cesar ([@cesarnr21](https://github.com/cesarnr21))\n- chaee ([@chaee](https://github.com/chaee))\n- lwileczek ([@lwileczek](https://github.com/lwileczek))\n- magicpotato ([@fortheday](https://github.com/fortheday))\n- motoacs ([@motoacs](https://github.com/motoacs))\n- muzian666 ([@muzian666](https://github.com/muzian666))\n- rburckner ([@rburckner](https://github.com/rburckner))\n- scorpion ([@scorpion](https://github.com/scorpion))\n- valantien ([@valantien](https://github.com/valantien))\n        \n</td></tr></tbody></table>\n</div>\n\n<h2 align=\"center\">OpenCollective Sponsors</h2>\n\nSupport 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)]\n\n<div align=\"center\">\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/0/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/1/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/2/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/3/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/4/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/5/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/6/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/7/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/8/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/9/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/10/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/10/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/11/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/11/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/12/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/12/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/13/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/13/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/14/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/14/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/15/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/15/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/16/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/16/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/17/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/17/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/18/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/18/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/19/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/19/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/20/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/20/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/21/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/21/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/22/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/22/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/23/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/23/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/24/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/24/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/25/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/25/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/26/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/26/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/27/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/27/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/28/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/28/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/29/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/29/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/30/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/30/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/31/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/31/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/32/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/32/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/33/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/33/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/34/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/34/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/35/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/35/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/36/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/36/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/37/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/37/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/38/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/38/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/39/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/39/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/40/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/40/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/41/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/41/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/42/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/42/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/43/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/43/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/44/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/44/avatar.svg\"></a>\n      </td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/40/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/45/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/41/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/46/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/42/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/47/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/43/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/48/avatar.svg\"></a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://opencollective.com/wikijs/sponsor/44/website\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/sponsor/49/avatar.svg\"></a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n</div>\n\n<h2 align=\"center\">Patreon Backers</h2>\n\nThank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/requarks)]\n\n<div align=\"center\">\n<table><tbody><tr><td>\n<img width=\"441\" height=\"1\" />\n\n- Aeternum\n- Al Romano\n- Alex Balabanov\n- Alex Milanov\n- Alex Zen\n- Arti Zirk\n- Ave\n- Brandon Curtis\n- Damien Hottelier\n- Daniel T. Holtzclaw\n- Dave 'Sri' Seah\n- djagoo\n- dz\n- Douglas Lassance\n- Ergoflix\n- Ernie Reid\n- Etienne\n- Flemis Jurgenheimer\n- Florent\n- Günter Pavlas\n- hong\n- Hope\n- Ian\n- Imari Childress\n- Iskander Callos\n  \n</td><td>\n<img width=\"441\" height=\"1\" />\n\n- Josh Stewart\n- Justin Dunsworth\n- Keir\n- Loïc CRAMPON\n- Ludgeir Ibanez\n- Lyn Matten\n- Mads Rosendahl\n- Mark Mansur\n- Matt Gedigian\n- Mike Ditton\n- Nate Figz\n- Patryk\n- Paul O'Fallon\n- Philipp Schürch\n- Tracey Duffy\n- Quaxim\n- Richeir\n- Sergio Navarro Fernández\n- Shad Narcher\n- ShadowVoyd\n- SmartNET.works\n- Stepan Sokolovskyi\n- Zach Crawford\n- Zach Maynard\n- 张白驹\n\n</td></tr></tbody></table>\n</div>\n\n<h2 align=\"center\">OpenCollective Backers</h2>\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/wikijs#backer)]\n\n<a href=\"https://opencollective.com/wikijs#backers\" target=\"_blank\"><img src=\"https://opencollective.com/wikijs/backers.svg?width=890\"></a>\n\n<h2 align=\"center\">Contributors</h2>\n\nThis project exists thanks to all the people who contribute. [[Contribute]](https://github.com/Requarks/wiki/blob/master/.github/CONTRIBUTING.md).\n<a href=\"https://github.com/Requarks/wiki/graphs/contributors\"><img src=\"https://opencollective.com/wikijs/contributors.svg?width=890\" /></a>\n\n<h2 align=\"center\">Special Thanks</h2>\n\n![Browserstack](https://js.wiki/legacy/logo_browserstack.png)  \n[Browserstack](https://www.browserstack.com/) for providing access to their great cross-browser testing tools.\n\n![Cloudflare](https://js.wiki/legacy/logo_cloudflare.png)  \n[Cloudflare](https://www.cloudflare.com/) for providing their great CDN, SSL and advanced networking services.\n\n![DigitalOcean](https://js.wiki/legacy/logo_digitalocean.png)  \n[DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site and APIs.\n\n![Icons8](https://static.requarks.io/logo/icons8-text-h40.png)  \n[Icons8](https://icons8.com/) for providing access to their beautiful icon sets.\n\n![Localazy](https://static.requarks.io/logo/localazy-h40.png)  \n[Localazy](https://localazy.com/) for providing access to their great localization service.\n\n![Lokalise](https://static.requarks.io/logo/lokalise-text-h40.png)  \n[Lokalise](https://lokalise.com/) for providing access to their great localization tool.\n\n![MacStadium](https://static.requarks.io/logo/macstadium-h40.png)  \n[MacStadium](https://www.macstadium.com) for providing access to their Mac hardware in the cloud.\n\n![Netlify](https://js.wiki/legacy/logo_netlify.png)  \n[Netlify](https://www.netlify.com) for providing hosting for our website.\n\n![ngrok](https://static.requarks.io/logo/ngrok-h40.png)  \n[ngrok](https://ngrok.com) for providing access to their great HTTP tunneling services.\n\n![Porkbun](https://static.requarks.io/logo/porkbun.png)  \n[Porkbun](https://www.porkbun.com) for providing domain registration services.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nWiki.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.\n\nIf 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.\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 2.x.x   | :white_check_mark: |\n| 1.x.x   | :x:                |\n\n## Reporting a Vulnerability\n\n> [!CAUTION]\n> **DO NOT CREATE A GITHUB ISSUE / DISCUSSION** to report a potential vulnerability / security problem. Instead, use the process below:\n\nSubmit a Vulnerability Report by filling in the form on https://github.com/requarks/wiki/security/advisories/new\n\nInclude as much details as possible, such as:\n- The version(s) of Wiki.js that are impacted\n- How to reproduce the vulnerability (step-by-step, screenshots or a video)\n- The platform / environment it occurs on (e.g. OS version, DB type + version, etc.)\n- Any potential fixes or reference code you think might be helpful in resolving the issue\n- Your GitHub username if you'd like to be included as a collaborator on the private fix branch\n\nThe 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.\n\n> [!NOTE]\n> 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.\n"
  },
  {
    "path": "client/.modernizrrc.js",
    "content": "module.exports = {\n  classPrefix: 'mdz-',\n  options: ['setClasses'],\n  'feature-detects': [\n    'css/backdropfilter'\n  ]\n}\n"
  },
  {
    "path": "client/client-app.js",
    "content": "/* global siteConfig */\n\nimport Vue from 'vue'\nimport VueRouter from 'vue-router'\nimport VueClipboards from 'vue-clipboards'\nimport { ApolloClient } from 'apollo-client'\nimport { BatchHttpLink } from 'apollo-link-batch-http'\nimport { ApolloLink, split } from 'apollo-link'\nimport { WebSocketLink } from 'apollo-link-ws'\nimport { ErrorLink } from 'apollo-link-error'\nimport { InMemoryCache } from 'apollo-cache-inmemory'\nimport { getMainDefinition } from 'apollo-utilities'\nimport VueApollo from 'vue-apollo'\nimport Vuetify from 'vuetify/lib'\nimport Velocity from 'velocity-animate'\nimport Vuescroll from 'vuescroll/dist/vuescroll-native'\nimport Hammer from 'hammerjs'\nimport moment from 'moment-timezone'\nimport VueMoment from 'vue-moment'\nimport store from './store'\nimport Cookies from 'js-cookie'\n\n// ====================================\n// Load Modules\n// ====================================\n\nimport boot from './modules/boot'\nimport localization from './modules/localization'\n\n// ====================================\n// Load Helpers\n// ====================================\n\nimport helpers from './helpers'\n\n// ====================================\n// Initialize Global Vars\n// ====================================\n\nwindow.WIKI = null\nwindow.boot = boot\nwindow.Hammer = Hammer\n\nmoment.locale(siteConfig.lang)\n\nstore.commit('user/REFRESH_AUTH')\n\n// ====================================\n// Initialize Apollo Client (GraphQL)\n// ====================================\n\nconst graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql'\nconst graphQLWSEndpoint = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/graphql-subscriptions'\n\nconst graphQLLink = ApolloLink.from([\n  new ErrorLink(({ graphQLErrors, networkError }) => {\n    if (graphQLErrors) {\n      let isAuthError = false\n      graphQLErrors.map(({ message, locations, path }) => {\n        if (message === `Forbidden`) {\n          isAuthError = true\n        }\n        console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)\n      })\n      store.commit('showNotification', {\n        style: 'red',\n        message: isAuthError ? `You are not authorized to access this resource.` : `An unexpected error occurred.`,\n        icon: 'alert'\n      })\n    }\n    if (networkError) {\n      console.error(networkError)\n      store.commit('showNotification', {\n        style: 'red',\n        message: `Network Error: ${networkError.message}`,\n        icon: 'alert'\n      })\n    }\n  }),\n  new BatchHttpLink({\n    includeExtensions: true,\n    uri: graphQLEndpoint,\n    credentials: 'include',\n    fetch: async (uri, options) => {\n      // Strip __typename fields from variables\n      let body = JSON.parse(options.body)\n      body = body.map(bd => {\n        return ({\n          ...bd,\n          variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })\n        })\n      })\n      options.body = JSON.stringify(body)\n\n      // Inject authentication token\n      const jwtToken = Cookies.get('jwt')\n      if (jwtToken) {\n        options.headers.Authorization = `Bearer ${jwtToken}`\n      }\n\n      const resp = await fetch(uri, options)\n\n      // Handle renewed JWT\n      const newJWT = resp.headers.get('new-jwt')\n      if (newJWT) {\n        Cookies.set('jwt', newJWT, { expires: 365, secure: window.location.protocol === 'https:' })\n      }\n      return resp\n    }\n  })\n])\n\nconst graphQLWSLink = new WebSocketLink({\n  uri: graphQLWSEndpoint,\n  options: {\n    reconnect: true,\n    lazy: true,\n    connectionParams: () => {\n      const token = Cookies.get('jwt')\n      return token ? { token } : {}\n    }\n  }\n})\n\nwindow.graphQL = new ApolloClient({\n  link: split(({ query }) => {\n    const { kind, operation } = getMainDefinition(query)\n    return kind === 'OperationDefinition' && operation === 'subscription'\n  }, graphQLWSLink, graphQLLink),\n  cache: new InMemoryCache(),\n  connectToDevTools: (process.env.node_env === 'development')\n})\n\n// ====================================\n// Initialize Vue Modules\n// ====================================\n\nVue.config.productionTip = false\n\nVue.use(VueRouter)\nVue.use(VueApollo)\nVue.use(VueClipboards)\nVue.use(localization.VueI18Next)\nVue.use(helpers)\nVue.use(Vuetify)\nVue.use(VueMoment, { moment })\nVue.use(Vuescroll)\n\nVue.prototype.Velocity = Velocity\n\n// ====================================\n// Register Vue Components\n// ====================================\n\nVue.component('Admin', () => import(/* webpackChunkName: \"admin\" */ './components/admin.vue'))\nVue.component('Comments', () => import(/* webpackChunkName: \"comments\" */ './components/comments.vue'))\nVue.component('Editor', () => import(/* webpackPrefetch: -100, webpackChunkName: \"editor\" */ './components/editor.vue'))\nVue.component('History', () => import(/* webpackChunkName: \"history\" */ './components/history.vue'))\nVue.component('Loader', () => import(/* webpackPrefetch: true, webpackChunkName: \"ui-extra\" */ './components/common/loader.vue'))\nVue.component('Login', () => import(/* webpackPrefetch: true, webpackChunkName: \"login\" */ './components/login.vue'))\nVue.component('NavHeader', () => import(/* webpackMode: \"eager\" */ './components/common/nav-header.vue'))\nVue.component('NewPage', () => import(/* webpackChunkName: \"new-page\" */ './components/new-page.vue'))\nVue.component('Notify', () => import(/* webpackMode: \"eager\" */ './components/common/notify.vue'))\nVue.component('NotFound', () => import(/* webpackChunkName: \"not-found\" */ './components/not-found.vue'))\nVue.component('PageSelector', () => import(/* webpackPrefetch: true, webpackChunkName: \"ui-extra\" */ './components/common/page-selector.vue'))\nVue.component('PageSource', () => import(/* webpackChunkName: \"source\" */ './components/source.vue'))\nVue.component('Profile', () => import(/* webpackChunkName: \"profile\" */ './components/profile.vue'))\nVue.component('Register', () => import(/* webpackChunkName: \"register\" */ './components/register.vue'))\nVue.component('SearchResults', () => import(/* webpackPrefetch: true, webpackChunkName: \"ui-extra\" */ './components/common/search-results.vue'))\nVue.component('SocialSharing', () => import(/* webpackPrefetch: true, webpackChunkName: \"ui-extra\" */ './components/common/social-sharing.vue'))\nVue.component('Tags', () => import(/* webpackChunkName: \"tags\" */ './components/tags.vue'))\nVue.component('Unauthorized', () => import(/* webpackChunkName: \"unauthorized\" */ './components/unauthorized.vue'))\nVue.component('VCardChin', () => import(/* webpackPrefetch: true, webpackChunkName: \"ui-extra\" */ './components/common/v-card-chin.vue'))\nVue.component('VCardInfo', () => import(/* webpackPrefetch: true, webpackChunkName: \"ui-extra\" */ './components/common/v-card-info.vue'))\nVue.component('Welcome', () => import(/* webpackChunkName: \"welcome\" */ './components/welcome.vue'))\n\nVue.component('NavFooter', () => import(/* webpackChunkName: \"theme\" */ './themes/' + siteConfig.theme + '/components/nav-footer.vue'))\nVue.component('Page', () => import(/* webpackChunkName: \"theme\" */ './themes/' + siteConfig.theme + '/components/page.vue'))\n\nlet bootstrap = () => {\n  // ====================================\n  // Notifications\n  // ====================================\n\n  window.addEventListener('beforeunload', () => {\n    store.dispatch('startLoading')\n  })\n\n  const apolloProvider = new VueApollo({\n    defaultClient: window.graphQL\n  })\n\n  // ====================================\n  // Bootstrap Vue\n  // ====================================\n\n  const i18n = localization.init()\n\n  let darkModeEnabled = siteConfig.darkMode\n  if ((store.get('user/appearance') || '').length > 0) {\n    darkModeEnabled = (store.get('user/appearance') === 'dark')\n  }\n\n  window.WIKI = new Vue({\n    el: '#root',\n    components: {},\n    mixins: [helpers],\n    apolloProvider,\n    store,\n    i18n,\n    vuetify: new Vuetify({\n      rtl: siteConfig.rtl,\n      theme: {\n        dark: darkModeEnabled\n      }\n    }),\n    mounted () {\n      this.$moment.locale(siteConfig.lang)\n      if ((store.get('user/dateFormat') || '').length > 0) {\n        this.$moment.updateLocale(this.$moment.locale(), {\n          longDateFormat: {\n            'L': store.get('user/dateFormat')\n          }\n        })\n      }\n      if ((store.get('user/timezone') || '').length > 0) {\n        this.$moment.tz.setDefault(store.get('user/timezone'))\n      }\n    }\n  })\n\n  // ----------------------------------\n  // Dispatch boot ready\n  // ----------------------------------\n\n  window.boot.notify('vue')\n}\n\nwindow.boot.onDOMReady(bootstrap)\n"
  },
  {
    "path": "client/client-setup.js",
    "content": "/* eslint-disable import/first */\nimport Vue from 'vue'\nimport Vuetify from 'vuetify/lib'\nimport boot from './modules/boot'\n/* eslint-enable import/first */\n\nwindow.WIKI = null\nwindow.boot = boot\n\nVue.use(Vuetify)\n\nVue.component('setup', () => import(/* webpackMode: \"eager\" */ './components/setup.vue'))\n\nlet bootstrap = () => {\n  window.WIKI = new Vue({\n    el: '#root',\n    vuetify: new Vuetify()\n  })\n}\n\nwindow.boot.onDOMReady(bootstrap)\n"
  },
  {
    "path": "client/components/admin/admin-analytics.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-line-chart.svg', alt='Analytics', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:analytics.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:analytics.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='primary', dark, dense)\n            .subtitle-1 {{$t('admin:analytics.providers')}}\n          v-list(two-line, dense).py-0\n            template(v-for='(str, idx) in providers')\n              v-list-item(:key='str.key', @click='selectedProvider = str.key', :disabled='!str.isAvailable')\n                v-list-item-avatar(size='24')\n                  v-icon(color='grey', v-if='!str.isAvailable') mdi-minus-box-outline\n                  v-icon(color='primary', v-else-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') mdi-checkbox-marked-outline\n                  v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') mdi-checkbox-blank-outline\n                v-list-item-content\n                  v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedProvider === str.key ? `primary--text` : ``)') {{ str.title }}\n                  v-list-item-subtitle: .caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === str.key ? `blue--text ` : ``)') {{ str.description }}\n                v-list-item-avatar(v-if='selectedProvider === str.key', size='24')\n                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right\n              v-divider(v-if='idx < providers.length - 1')\n\n      v-flex(xs12, lg9)\n\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{provider.title}}\n            v-spacer\n            v-switch(\n              dark\n              color='blue lighten-5'\n              label='Active'\n              v-model='provider.isEnabled'\n              hide-details\n              inset\n              )\n          v-card-info(color='blue')\n            div\n              div {{provider.description}}\n              span.caption: a(:href='provider.website') {{provider.website}}\n            v-spacer\n            .admin-providerlogo\n              img(:src='provider.logo', :alt='provider.title')\n          v-card-text\n            v-form\n              .overline.pb-5 {{$t('admin:analytics.providerConfiguration')}}\n              .body-1.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:analytics.providerNoConfiguration')}}\n              template(v-else, v-for='cfg in provider.config')\n                v-select(\n                  v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                  outlined\n                  :items='cfg.value.enum'\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                )\n                v-switch.mb-3(\n                  v-else-if='cfg.value.type === \"boolean\"'\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  color='primary'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  inset\n                  )\n                v-textarea(\n                  v-else-if='cfg.value.type === \"string\" && cfg.value.multiline'\n                  outlined\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                  )\n                v-text-field(\n                  v-else\n                  outlined\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                  )\n\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport providersQuery from 'gql/admin/analytics/analytics-query-providers.gql'\nimport providersSaveMutation from 'gql/admin/analytics/analytics-mutation-save-providers.gql'\n\nexport default {\n  data() {\n    return {\n      providers: [],\n      selectedProvider: '',\n      provider: {}\n    }\n  },\n  watch: {\n    selectedProvider(newValue, oldValue) {\n      this.provider = _.find(this.providers, ['key', newValue]) || {}\n    },\n    providers(newValue, oldValue) {\n      this.selectedProvider = 'google'\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.providers.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('admin:analytics.refreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-analytics-saveproviders')\n      try {\n        await this.$apollo.mutate({\n          mutation: providersSaveMutation,\n          variables: {\n            providers: this.providers.map(str => _.pick(str, [\n              'isEnabled',\n              'key',\n              'config'\n            ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))\n          }\n        })\n        this.$store.commit('showNotification', {\n          message: this.$t('admin:analytics.saveSuccess'),\n          style: 'success',\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-analytics-saveproviders')\n    }\n  },\n  apollo: {\n    providers: {\n      query: providersQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.analytics.providers).map(str => ({\n        ...str,\n        config: _.sortBy(str.config.map(cfg => ({\n          ...cfg,\n          value: JSON.parse(cfg.value)\n        })), [t => t.value.order])\n      })),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-analytics-refresh')\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-api-create.vue",
    "content": "<template lang=\"pug\">\n  div\n    v-dialog(v-model='isShown', max-width='650', persistent)\n      v-card\n        .dialog-header.is-short\n          v-icon.mr-3(color='white') mdi-plus\n          span {{$t('admin:api.newKeyTitle')}}\n        v-card-text.pt-5\n          v-text-field(\n            outlined\n            prepend-icon='mdi-format-title'\n            v-model='name'\n            :label='$t(`admin:api.newKeyName`)'\n            persistent-hint\n            ref='keyNameInput'\n            :hint='$t(`admin:api.newKeyNameHint`)'\n            counter='255'\n            )\n          v-select.mt-3(\n            :items='expirations'\n            outlined\n            prepend-icon='mdi-clock'\n            v-model='expiration'\n            :label='$t(`admin:api.newKeyExpiration`)'\n            :hint='$t(`admin:api.newKeyExpirationHint`)'\n            persistent-hint\n            )\n          v-divider.mt-4\n          v-subheader.pl-2: strong.indigo--text {{$t('admin:api.newKeyPermissionScopes')}}\n          v-list.pl-8(nav)\n            v-list-item-group(v-model='fullAccess')\n              v-list-item(\n                :value='true'\n                active-class='indigo--text'\n                )\n                template(v-slot:default='{ active, toggle }')\n                  v-list-item-action\n                    v-checkbox(\n                      :input-value='active'\n                      :true-value='true'\n                      color='indigo'\n                      @click='toggle'\n                    )\n                  v-list-item-content\n                    v-list-item-title {{$t('admin:api.newKeyFullAccess')}}\n            v-divider.mt-3\n            v-subheader.caption.indigo--text {{$t('admin:api.newKeyGroupPermissions')}}\n            v-list-item\n              v-select(\n                :disabled='fullAccess'\n                :items='groups'\n                item-text='name'\n                item-value='id'\n                outlined\n                color='indigo'\n                v-model='group'\n                :label='$t(`admin:api.newKeyGroup`)'\n                :hint='$t(`admin:api.newKeyGroupHint`)'\n                persistent-hint\n                )\n        v-card-chin\n          v-spacer\n          v-btn(text, @click='isShown = false', :disabled='loading') {{$t('common:actions.cancel')}}\n          v-btn.px-3(depressed, color='primary', @click='generate', :loading='loading')\n            v-icon(left) mdi-chevron-right\n            span {{$t('common:actions.generate')}}\n\n    v-dialog(\n      v-model='isCopyKeyDialogShown'\n      max-width='750'\n      persistent\n      overlay-color='blue darken-5'\n      overlay-opacity='.9'\n      )\n      v-card\n        v-toolbar(dense, flat, color='primary', dark) {{$t('admin:api.newKeyTitle')}}\n        v-card-text.pt-5\n          .body-2.text-center\n            i18next(tag='span', path='admin:api.newKeyCopyWarn')\n              strong(place='bold') {{$t('admin:api.newKeyCopyWarnBold')}}\n          v-textarea.mt-3(\n            ref='keyContentsIpt'\n            filled\n            no-resize\n            readonly\n            v-model='key'\n            :rows='10'\n            hide-details\n          )\n        v-card-chin\n          v-spacer\n          v-btn.px-3(depressed, dark, color='primary', @click='isCopyKeyDialogShown = false') {{$t('common:actions.close')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nimport groupsQuery from 'gql/admin/users/users-query-groups.gql'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      loading: false,\n      name: '',\n      expiration: '1y',\n      fullAccess: true,\n      groups: [],\n      group: null,\n      isCopyKeyDialogShown: false,\n      key: ''\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    expirations() {\n      return [\n        { value: '30d', text: this.$t('admin:api.expiration30d') },\n        { value: '90d', text: this.$t('admin:api.expiration90d') },\n        { value: '180d', text: this.$t('admin:api.expiration180d') },\n        { value: '1y', text: this.$t('admin:api.expiration1y') },\n        { value: '3y', text: this.$t('admin:api.expiration3y') }\n      ]\n    }\n  },\n  watch: {\n    value (newValue, oldValue) {\n      if (newValue) {\n        setTimeout(() => {\n          this.$refs.keyNameInput.focus()\n        }, 400)\n      }\n    }\n  },\n  methods: {\n    async generate () {\n      try {\n        if (_.trim(this.name).length < 2 || this.name.length > 255) {\n          throw new Error(this.$t('admin:api.newKeyNameError'))\n        } else if (!this.fullAccess && !this.group) {\n          throw new Error(this.$t('admin:api.newKeyGroupError'))\n        } else if (!this.fullAccess && this.group === 2) {\n          throw new Error(this.$t('admin:api.newKeyGuestGroupError'))\n        }\n      } catch (err) {\n        return this.$store.commit('showNotification', {\n          style: 'red',\n          message: err,\n          icon: 'alert'\n        })\n      }\n\n      this.loading = true\n\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($name: String!, $expiration: String!, $fullAccess: Boolean!, $group: Int) {\n              authentication {\n                createApiKey (name: $name, expiration: $expiration, fullAccess: $fullAccess, group: $group) {\n                  key\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            name: this.name,\n            expiration: this.expiration,\n            fullAccess: (this.fullAccess === true),\n            group: this.group\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-create')\n          }\n        })\n        if (_.get(resp, 'data.authentication.createApiKey.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('admin:api.newKeySuccess'),\n            icon: 'check'\n          })\n\n          this.name = ''\n          this.expiration = '1y'\n          this.fullAccess = true\n          this.group = null\n          this.isShown = false\n          this.$emit('refresh')\n\n          this.key = _.get(resp, 'data.authentication.createApiKey.key', '???')\n          this.isCopyKeyDialogShown = true\n\n          setTimeout(() => {\n            this.$refs.keyContentsIpt.$refs.input.select()\n          }, 400)\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: _.get(resp, 'data.authentication.createApiKey.responseResult.message', 'An unexpected error occurred.'),\n            icon: 'alert'\n          })\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.loading = false\n    }\n  },\n  apollo: {\n    groups: {\n      query: groupsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-groups-refresh')\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-api.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-rest-api.svg', alt='API', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('admin:api.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft {{$t('admin:api.subtitle')}}\n          v-spacer\n          template(v-if='enabled')\n            status-indicator.mr-3(positive, pulse)\n            .caption.green--text.animated.fadeInLeft {{$t('admin:api.enabled')}}\n          template(v-else)\n            status-indicator.mr-3(negative, pulse)\n            .caption.red--text.animated.fadeInLeft {{$t('admin:api.disabled')}}\n          v-spacer\n          v-btn.mr-3.animated.fadeInDown.wait-p2s(outlined, color='grey', icon, @click='refresh')\n            v-icon mdi-refresh\n          v-btn.mr-3.animated.fadeInDown.wait-p1s(:color='enabled ? `red` : `green`', depressed, @click='globalSwitch', dark, :loading='isToggleLoading')\n            v-icon(left) mdi-power\n            span(v-if='!enabled') {{$t('admin:api.enableButton')}}\n            span(v-else) {{$t('admin:api.disableButton')}}\n          v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newKey', dark)\n            v-icon(left) mdi-plus\n            span {{$t('admin:api.newKeyButton')}}\n        v-card.mt-3.animated.fadeInUp\n          v-simple-table(v-if='keys && keys.length > 0')\n            template(v-slot:default)\n              thead\n                tr.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-5`')\n                  th {{$t('admin:api.headerName')}}\n                  th {{$t('admin:api.headerKeyEnding')}}\n                  th {{$t('admin:api.headerExpiration')}}\n                  th {{$t('admin:api.headerCreated')}}\n                  th {{$t('admin:api.headerLastUpdated')}}\n                  th(width='100') {{$t('admin:api.headerRevoke')}}\n              tbody\n                tr(v-for='key of keys', :key='`key-` + key.id')\n                  td\n                    strong(:class='key.isRevoked ? `red--text` : ``') {{ key.name }}\n                    em.caption.ml-1.red--text(v-if='key.isRevoked') (revoked)\n                  td.caption {{ key.keyShort }}\n                  td(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{ key.expiration | moment('LL') }}\n                  td {{ key.createdAt | moment('calendar') }}\n                  td {{ key.updatedAt | moment('calendar') }}\n                  td: v-btn(icon, @click='revoke(key)', :disabled='key.isRevoked'): v-icon(color='error') mdi-cancel\n          v-card-text(v-else)\n            v-alert.mb-0(icon='mdi-information', :value='true', outlined, color='info') {{$t('admin:api.noKeyInfo')}}\n\n    create-api-key(v-model='isCreateDialogShown', @refresh='refresh(false)')\n\n    v-dialog(v-model='isRevokeConfirmDialogShown', max-width='500', persistent)\n      v-card\n        .dialog-header.is-red {{$t('admin:api.revokeConfirm')}}\n        v-card-text.pa-4\n          i18next(tag='span', path='admin:api.revokeConfirmText')\n            strong(place='name') {{ current.name }}\n        v-card-actions\n          v-spacer\n          v-btn(text, @click='isRevokeConfirmDialogShown = false', :disabled='revokeLoading') {{$t('common:actions.cancel')}}\n          v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin:api.revoke')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { StatusIndicator } from 'vue-status-indicator'\n\nimport CreateApiKey from './admin-api-create.vue'\n\nexport default {\n  components: {\n    StatusIndicator,\n    CreateApiKey\n  },\n  data() {\n    return {\n      enabled: false,\n      isToggleLoading: false,\n      keys: [],\n      isCreateDialogShown: false,\n      isRevokeConfirmDialogShown: false,\n      revokeLoading: false,\n      current: {}\n    }\n  },\n  methods: {\n    async refresh (notify = true) {\n      this.$apollo.queries.keys.refetch()\n      if (notify) {\n        this.$store.commit('showNotification', {\n          message: this.$t('admin:api.refreshSuccess'),\n          style: 'success',\n          icon: 'cached'\n        })\n      }\n    },\n    async globalSwitch () {\n      this.isToggleLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($enabled: Boolean!) {\n              authentication {\n                setApiState (enabled: $enabled) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            enabled: !this.enabled\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-toggle')\n          }\n        })\n        if (_.get(resp, 'data.authentication.setApiState.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.enabled ? this.$t('admin:api.toggleStateDisabledSuccess') : this.$t('admin:api.toggleStateEnabledSuccess'),\n            icon: 'check'\n          })\n          await this.$apollo.queries.enabled.refetch()\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: _.get(resp, 'data.authentication.setApiState.responseResult.message', 'An unexpected error occurred.'),\n            icon: 'alert'\n          })\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.isToggleLoading = false\n    },\n    async newKey () {\n      this.isCreateDialogShown = true\n    },\n    revoke (key) {\n      this.current = key\n      this.isRevokeConfirmDialogShown = true\n    },\n    async revokeConfirm () {\n      this.revokeLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($id: Int!) {\n              authentication {\n                revokeApiKey (id: $id) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.current.id\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-revoke')\n          }\n        })\n        if (_.get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('admin:api.revokeSuccess'),\n            icon: 'check'\n          })\n          this.refresh(false)\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: _.get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occurred.'),\n            icon: 'alert'\n          })\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.isRevokeConfirmDialogShown = false\n      this.revokeLoading = false\n    }\n  },\n  apollo: {\n    enabled: {\n      query: gql`\n        {\n          authentication {\n            apiState\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.authentication.apiState,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-state-refresh')\n      }\n    },\n    keys: {\n      query: gql`\n        {\n          authentication {\n            apiKeys {\n              id\n              name\n              keyShort\n              expiration\n              isRevoked\n              createdAt\n              updatedAt\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.authentication.apiKeys,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-keys-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-auth.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:auth.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:auth.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/auth', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.animated.fadeInDown.wait-p2s.mx-3(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='teal', dark, dense)\n            .subtitle-1 {{$t('admin:auth.activeStrategies')}}\n          v-list(two-line, dense).py-0\n            draggable(\n              v-model='activeStrategies'\n              handle='.is-handle'\n              direction='vertical'\n              )\n              transition-group\n                v-list-item(\n                  v-for='(str, idx) in activeStrategies'\n                  :key='str.key'\n                  @click='selectedStrategy = str.key'\n                  :class='selectedStrategy === str.key ? ($vuetify.theme.dark ? `grey darken-5` : `teal lighten-5`) : ``'\n                  )\n                  v-list-item-avatar.is-handle(size='24')\n                    v-icon(:color='selectedStrategy === str.key ? `teal` : `grey`') mdi-drag-horizontal\n                  v-list-item-content\n                    v-list-item-title.body-2(:class='selectedStrategy === str.key ? `teal--text` : ``') {{ str.displayName }}\n                    v-list-item-subtitle: .caption(:class='selectedStrategy === str.key ? `teal--text ` : ``') {{ str.strategy.title }}\n                  v-list-item-avatar(v-if='selectedStrategy === str.key', size='24')\n                    v-icon.animated.fadeInLeft(color='teal', large) mdi-chevron-right\n          v-card-chin\n            v-menu(offset-y, bottom, min-width='250px', max-width='550px', max-height='50vh', style='flex: 1 1;', center)\n              template(v-slot:activator='{ on }')\n                v-btn(v-on='on', color='primary', depressed, block)\n                  v-icon(left) mdi-plus\n                  span {{$t('admin:auth.addStrategy')}}\n              v-list(dense)\n                template(v-for='(str, idx) of strategies')\n                  v-list-item(\n                    :key='str.key'\n                    :disabled='str.isDisabled'\n                    @click='addStrategy(str)'\n                    )\n                    v-list-item-avatar(height='24', width='48', tile)\n                      v-img(:src='str.logo', width='48px', height='24px', contain, :style='str.isDisabled ? `opacity: .25;` : ``')\n                    v-list-item-content\n                      v-list-item-title {{str.title}}\n                      v-list-item-subtitle: .caption(:style='str.isDisabled ? `opacity: .4;` : ``') {{str.description}}\n                  v-divider(v-if='idx < strategies.length - 1')\n\n      v-flex(xs12, lg9)\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{strategy.displayName}} #[em ({{strategy.strategy.title}})]\n            v-spacer\n            v-btn(small, outlined, dark, color='white', :disabled='strategy.key === `local`', @click='deleteStrategy()')\n              v-icon(left) mdi-close\n              span {{$t('common:actions.delete')}}\n          v-card-info(color='blue')\n            div\n              span {{strategy.strategy.description}}\n              .caption: a(:href='strategy.strategy.website') {{strategy.strategy.website}}\n            v-spacer\n            .admin-providerlogo\n              img(:src='strategy.strategy.logo', :alt='strategy.strategy.title')\n          v-card-text\n            .row\n              .col-8\n                v-text-field(\n                  outlined\n                  :label='$t(`admin:auth.displayName`)'\n                  v-model='strategy.displayName'\n                  prepend-icon='mdi-format-title'\n                  :hint='$t(`admin:auth.displayNameHint`)'\n                  persistent-hint\n                  )\n              .col-4\n                v-switch.mt-1(\n                  :label='$t(`admin:auth.strategyIsEnabled`)'\n                  v-model='strategy.isEnabled'\n                  color='primary'\n                  prepend-icon='mdi-power'\n                  :hint='$t(`admin:auth.strategyIsEnabledHint`)'\n                  persistent-hint\n                  inset\n                  :disabled='strategy.key === `local`'\n                  )\n            template(v-if='strategy.config && Object.keys(strategy.config).length > 0')\n              v-divider\n              .overline.my-5 {{$t('admin:auth.strategyConfiguration')}}\n              .pr-3\n                template(v-for='cfg in strategy.config')\n                  v-select.mb-3(\n                    v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                    outlined\n                    :items='cfg.value.enum'\n                    :key='cfg.key'\n                    :label='cfg.value.title'\n                    v-model='cfg.value.value'\n                    prepend-icon='mdi-cog-box'\n                    :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                    persistent-hint\n                    :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                    :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'\n                  )\n                  v-switch.mb-6(\n                    v-else-if='cfg.value.type === \"boolean\"'\n                    :key='cfg.key'\n                    :label='cfg.value.title'\n                    v-model='cfg.value.value'\n                    color='primary'\n                    prepend-icon='mdi-cog-box'\n                    :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                    persistent-hint\n                    inset\n                    )\n                  v-textarea.mb-3(\n                    v-else-if='cfg.value.type === \"string\" && cfg.value.multiline'\n                    outlined\n                    :key='cfg.key'\n                    :label='cfg.value.title'\n                    v-model='cfg.value.value'\n                    prepend-icon='mdi-cog-box'\n                    :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                    persistent-hint\n                    :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                    )\n                  v-text-field.mb-3(\n                    v-else\n                    outlined\n                    :key='cfg.key'\n                    :label='cfg.value.title'\n                    v-model='cfg.value.value'\n                    prepend-icon='mdi-cog-box'\n                    :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                    persistent-hint\n                    :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                    :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'\n                    )\n            v-divider\n            .overline.my-5 {{$t('admin:auth.registration')}}\n            .pr-3\n              v-switch.ml-3(\n                v-model='strategy.selfRegistration'\n                :label='$t(`admin:auth.selfRegistration`)'\n                color='primary'\n                :hint='$t(`admin:auth.selfRegistrationHint`)'\n                persistent-hint\n                inset\n              )\n              v-combobox.ml-3.mt-5(\n                :label='$t(`admin:auth.domainsWhitelist`)'\n                v-model='strategy.domainWhitelist'\n                prepend-icon='mdi-email-check-outline'\n                outlined\n                :disabled='!strategy.selfRegistration'\n                :hint='$t(`admin:auth.domainsWhitelistHint`)'\n                persistent-hint\n                small-chips\n                deletable-chips\n                clearable\n                multiple\n                chips\n                )\n              v-autocomplete.mt-3.ml-3(\n                outlined\n                :disabled='!strategy.selfRegistration'\n                :items='groups'\n                item-text='name'\n                item-value='id'\n                :label='$t(`admin:auth.autoEnrollGroups`)'\n                v-model='strategy.autoEnrollGroups'\n                prepend-icon='mdi-account-group'\n                :hint='$t(`admin:auth.autoEnrollGroupsHint`)'\n                small-chips\n                persistent-hint\n                deletable-chips\n                clearable\n                multiple\n                chips\n                )\n\n        v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s(v-if='selectedStrategy !== `local`')\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{$t('admin:auth.configReference')}}\n          v-card-text\n            .body-2 {{$t('admin:auth.configReferenceSubtitle')}}\n            v-alert.mt-3.radius-7(v-if='host.length < 8', color='red', outlined, :value='true', icon='mdi-alert')\n              i18next(path='admin:auth.siteUrlNotSetup', tag='span')\n                strong(place='siteUrl') {{$t('admin:general.siteUrl')}}\n                strong(place='general') {{$t('admin:general.title')}}\n            .pa-3.mt-3.radius-7.grey(v-else, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`')\n              .body-2: strong {{$t('admin:auth.allowedWebOrigins')}}\n              .body-2 {{host}}\n              v-divider.my-3\n              .body-2: strong {{$t('admin:auth.callbackUrl')}}\n              .body-2 {{host}}/login/{{strategy.key}}/callback\n              v-divider.my-3\n              .body-2: strong {{$t('admin:auth.loginUrl')}}\n              .body-2 {{host}}/login\n              v-divider.my-3\n              .body-2: strong {{$t('admin:auth.logoutUrl')}}\n              .body-2 {{host}}\n              v-divider.my-3\n              .body-2: strong {{$t('admin:auth.tokenEndpointAuthMethod')}}\n              .body-2 HTTP-POST\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { v4 as uuid } from 'uuid'\n\nimport groupsQuery from 'gql/admin/auth/auth-query-groups.gql'\nimport hostQuery from 'gql/admin/auth/auth-query-host.gql'\n\nimport draggable from 'vuedraggable'\n\nexport default {\n  components: {\n    draggable\n  },\n  filters: {\n    startCase(val) { return _.startCase(val) }\n  },\n  data() {\n    return {\n      groups: [],\n      strategies: [],\n      activeStrategies: [],\n      selectedStrategy: '',\n      host: '',\n      strategy: {\n        strategy: {}\n      }\n    }\n  },\n  watch: {\n    selectedStrategy(newValue, oldValue) {\n      this.strategy = _.find(this.activeStrategies, ['key', newValue]) || {}\n    },\n    activeStrategies(newValue, oldValue) {\n      this.selectedStrategy = 'local'\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.strategies.refetch()\n      await this.$apollo.queries.activeStrategies.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('admin:auth.refreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    addStrategy (str) {\n      const newStr = {\n        key: uuid(),\n        strategy: str,\n        config: str.props.map(c => ({\n          key: c.key,\n          value: {\n            ...c,\n            value: c.default\n          }\n        })),\n        order: this.activeStrategies.length,\n        isEnabled: true,\n        displayName: str.title,\n        selfRegistration: false,\n        domainWhitelist: [],\n        autoEnrollGroups: []\n      }\n      this.activeStrategies = [...this.activeStrategies, newStr]\n      this.$nextTick(() => {\n        this.selectedStrategy = newStr.key\n      })\n    },\n    deleteStrategy () {\n      this.activeStrategies = _.reject(this.activeStrategies, ['key', this.strategy.key])\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-auth-savestrategies')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation($strategies: [AuthenticationStrategyInput]!) {\n              authentication {\n                updateStrategies(strategies: $strategies) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            strategies: this.activeStrategies.map((str, idx) => ({\n              key: str.key,\n              strategyKey: str.strategy.key,\n              displayName: str.displayName,\n              order: idx,\n              isEnabled: str.isEnabled,\n              config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })})),\n              selfRegistration: str.selfRegistration,\n              domainWhitelist: str.domainWhitelist,\n              autoEnrollGroups: str.autoEnrollGroups\n            }))\n          }\n        })\n        if (_.get(resp, 'data.authentication.updateStrategies.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('admin:auth.saveSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(_.get(resp, 'data.authentication.updateStrategies.responseResult.message', this.$t('common:error.unexpected')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-auth-savestrategies')\n    }\n  },\n  apollo: {\n    strategies: {\n      query: gql`\n        query {\n          authentication {\n            strategies {\n              key\n              title\n              description\n              isAvailable\n              useForm\n              logo\n              website\n              props {\n                key\n                value\n              }\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.get(data, 'authentication.strategies', []).map(str => ({\n        ...str,\n        isDisabled: !str.isAvailable || str.key === `local`,\n        props: _.sortBy(str.props.map(cfg => ({\n          key: cfg.key,\n          ...JSON.parse(cfg.value)\n        })), [t => t.order])\n      })),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-strategies-refresh')\n      }\n    },\n    activeStrategies: {\n      query: gql`\n        query {\n          authentication {\n            activeStrategies {\n              key\n              strategy {\n                key\n                title\n                description\n                useForm\n                logo\n                website\n              }\n              config {\n                key\n                value\n              }\n              order\n              isEnabled\n              displayName\n              selfRegistration\n              domainWhitelist\n              autoEnrollGroups\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.sortBy(_.get(data, 'authentication.activeStrategies', []).map(str => ({\n        ...str,\n        config: _.sortBy(str.config.map(cfg => ({\n          ...cfg,\n          value: JSON.parse(cfg.value)\n        })), [t => t.value.order])\n      })), ['order']),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-activestrategies-refresh')\n      }\n    },\n    groups: {\n      query: groupsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')\n      }\n    },\n    host: {\n      query: hostQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.site.config.host),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-host-refresh')\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-comments.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-chat-bubble.svg', alt='Comments', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('admin:comments.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:comments.subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/comments', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='primary', dark, dense)\n            .subtitle-1 {{$t('admin:comments.provider')}}\n          v-list.py-0(two-line, dense)\n            template(v-for='(provider, idx) in providers')\n              v-list-item(:key='provider.key', @click='selectedProvider = provider.key', :disabled='!provider.isAvailable')\n                v-list-item-avatar(size='24')\n                  v-icon(color='grey', v-if='!provider.isAvailable') mdi-minus-box-outline\n                  v-icon(color='primary', v-else-if='provider.key === selectedProvider') mdi-checkbox-marked-circle-outline\n                  v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline\n                v-list-item-content\n                  v-list-item-title.body-2(:class='!provider.isAvailable ? `grey--text` : (selectedProvider === provider.key ? `primary--text` : ``)') {{ provider.title }}\n                  v-list-item-subtitle: .caption(:class='!provider.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === provider.key ? `blue--text ` : ``)') {{ provider.description }}\n                v-list-item-avatar(v-if='selectedProvider === provider.key', size='24')\n                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right\n              v-divider(v-if='idx < providers.length - 1')\n\n      v-flex(lg9, xs12)\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{provider.title}}\n          v-card-info(color='blue')\n            div\n              div {{provider.description}}\n              span.caption: a(:href='provider.website') {{provider.website}}\n            v-spacer\n            .admin-providerlogo\n              img(:src='provider.logo', :alt='provider.title')\n          v-card-text\n            .overline.my-5 {{$t('admin:comments.providerConfig')}}\n            .body-2.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:comments.providerNoConfig')}}\n            template(v-else, v-for='cfg in provider.config')\n              v-select.mb-3(\n                v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                outlined\n                :items='cfg.value.enum'\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'\n              )\n              v-switch.mb-6(\n                v-else-if='cfg.value.type === \"boolean\"'\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                color='primary'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                inset\n                )\n              v-textarea.mb-3(\n                v-else-if='cfg.value.type === \"string\" && cfg.value.multiline'\n                outlined\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                )\n              v-text-field.mb-3(\n                v-else\n                outlined\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'\n                )\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nexport default {\n  data() {\n    return {\n      providers: [],\n      selectedProvider: '',\n      provider: {}\n    }\n  },\n  watch: {\n    selectedProvider(newValue, oldValue) {\n      this.provider = _.find(this.providers, ['key', newValue]) || {}\n    },\n    providers(newValue, oldValue) {\n      this.selectedProvider = _.get(_.find(this.providers, 'isEnabled'), 'key', 'db')\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.providers.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('admin:comments.listRefreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-comments-saveproviders')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation($providers: [CommentProviderInput]!) {\n              comments {\n                updateProviders(providers: $providers) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            providers: this.providers.map(tgt => ({\n              isEnabled: tgt.key === this.selectedProvider,\n              key: tgt.key,\n              config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))\n            }))\n          }\n        })\n        if (_.get(resp, 'data.comments.updateProviders.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('admin:comments.configSaveSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(_.get(resp, 'data.comments.updateProviders.responseResult.message', this.$t('common:error.unexpected')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-comments-saveproviders')\n    }\n  },\n  apollo: {\n    providers: {\n      query: gql`\n        query {\n          comments {\n            providers {\n              isEnabled\n              key\n              title\n              description\n              logo\n              website\n              isAvailable\n              config {\n                key\n                value\n              }\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.comments.providers).map(str => ({\n        ...str,\n        config: _.sortBy(str.config.map(cfg => ({\n          ...cfg,\n          value: JSON.parse(cfg.value)\n        })), [t => t.value.order])\n      })),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-comments-refresh')\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-contribute.vue",
    "content": "<template lang='pug'>\n  v-container.admin-contribute(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:contribute.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:contribute.subtitle') }}\n        v-card.mt-3.animated.fadeInUp\n          v-card-text\n            i18next.body-2.pl-3(path='admin:contribute.openSource', tag='div')\n              v-icon(color='red') mdi-heart\n              a(href='https://requarks.io', target='_blank') requarks.io\n              a(href='https://github.com/Requarks/wiki/graphs/contributors', target='_blank') {{ $t('admin:contribute.openSourceContributors') }}\n            .body-2.pt-3.pl-3 {{ $t('admin:contribute.needYourHelp') }}\n            v-divider.mt-3\n            v-subheader.subtitle-2 {{ $t('admin:contribute.fundOurWork') }}\n            v-tabs.mx-3.radius-7.admin-contribute-tabs(\n              centered\n              fixed-tabs\n              background-color='primary'\n              color='white'\n              dark\n              slider-color='#FFF'\n              icons-and-text\n              )\n              v-tab\n                span GitHub\n                v-icon.my-1(size='24') mdi-github\n              v-tab\n                span Patreon\n                img.my-1(src='/_assets/svg/icon-patreon.svg', style='height: 24px;')\n              v-tab\n                span OpenCollective\n                img.my-1(src='/_assets/svg/icon-opencollective.svg', style='height: 24px;')\n              v-tab\n                span PayPal\n                img.my-1(src='/_assets/svg/icon-paypal.svg', style='height: 24px;')\n              v-tab\n                span Ethereum\n                img.my-1(src='/_assets/svg/icon-ethereum.svg', style='height: 24px;')\n              v-tab\n                span T-Shirts\n                img.my-1(src='/_assets/svg/icon-t-shirt.svg', style='height: 24px;')\n              v-tab-item(:transition='false', :reverse-transition='false')\n                .body-2.pa-3 {{ $t('admin:contribute.github') }}\n                a.ml-3(href='https://github.com/users/NGPixel/sponsorship', :title='$t(`admin:contribute.becomeASponsor`)')\n                  img(src='/_assets/img/donate_github.svg', :alt='$t(`admin:contribute.becomeASponsor`)' style='width:200px;')\n              v-tab-item(:transition='false', :reverse-transition='false')\n                .body-2.pa-3 {{ $t('admin:contribute.patreon') }}\n                a.ml-3(href='https://www.patreon.com/bePatron?u=16744039', :title='$t(`admin:contribute.becomeAPatron`)')\n                  img(src='/_assets/img/donate_patreon.png', :alt='$t(`admin:contribute.becomeAPatron`)' style='width:200px;')\n              v-tab-item(:transition='false', :reverse-transition='false')\n                .body-2.pa-3 {{ $t('admin:contribute.openCollective') }}\n                a.ml-3(href='https://opencollective.com/wikijs/donate', :title='$t(`admin:contribute.makeADonation`)')\n                  img(src='/_assets/img/donate_opencollective.png', :alt='$t(`admin:contribute.makeADonation`)' style='width:300px;')\n              v-tab-item(:transition='false', :reverse-transition='false')\n                .body-2.pa-3 {{ $t('admin:contribute.paypal') }}\n                .ml-3\n                  form(action='https://www.paypal.com/cgi-bin/webscr', method='post', target='_top')\n                    input(type='hidden', name='cmd', value='_s-xclick')\n                    input(type='hidden', name='hosted_button_id', value='FLV5X255Z9CJU')\n                    input(type='image', src='/_assets/img/donate_paypal.png', border='0', name='submit', title='PayPal - The safer, easier way to pay online!', alt='Donate with PayPal button')\n                    img(alt='', border='0', src='https://www.paypal.com/en_CA/i/scr/pixel.gif', width='1', height='1')\n              v-tab-item(:transition='false', :reverse-transition='false')\n                .body-2.pa-3 {{ $t('admin:contribute.ethereum') }}\n                .ml-3\n                  .admin-contribute-ethaddress\n                    strong Ethereum Address\n                    span 0xE1d55C19aE86f6Bcbfb17e7f06aCe96BdBb22Cb5\n                  div: img(src='/_assets/img/donate_eth_qr.png')\n              v-tab-item(:transition='false', :reverse-transition='false')\n                .body-2.pa-3 {{ $t('admin:contribute.tshirts') }}\n                v-card-actions.ml-2\n                  v-btn(outlined, :color='$vuetify.theme.dark ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large)\n                    v-icon(left) mdi-tshirt-crew\n                    span {{ $t('admin:contribute.shop') }}\n            v-divider.mt-3\n            v-subheader.subtitle-2  {{ $t('admin:contribute.contribute') }}\n            .body-2.pl-3\n              ul\n                i18next(path='admin:contribute.submitAnIdea', tag='li')\n                  a(href='https://requests.requarks.io/wiki', target='_blank') {{ $t('admin:contribute.submitAnIdeaLink') }}\n                i18next(path='admin:contribute.foundABug', tag='li')\n                  a(href='https://github.com/Requarks/wiki/issues', target='_blank') Github\n                i18next(path='admin:contribute.helpTranslate', tag='li')\n                  a(href='https://wiki.requarks.io/slack', target='_blank') Slack\n            v-divider.mt-3\n            v-subheader.subtitle-2  {{ $t('admin:contribute.spreadTheWord') }}\n            .body-2.pl-3\n              ul\n                li {{ $t('admin:contribute.talkToFriends') }}\n                i18next(path='admin:contribute.followUsOnTwitter', tag='li')\n                  a(href='https://twitter.com/requarks', target='_blank') Twitter\n          v-toolbar(color='indigo', dense, dark)\n            .subtitle-1 Sponsors &amp; Backers\n          v-container.pa-5.grey(fluid, :class='$vuetify.theme.dark ? `darken-3` : `lighten-4`')\n            v-progress-circular(indeterminate, color='indigo', size='24', width='2', v-if='backers.length < 1')\n            v-row(dense)\n              v-col(cols='12', lg='6', xl='4', v-for='(backer, idx) in backers', :key='backer.id')\n                v-card.grey(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-2`')\n                  v-list-item\n                    v-list-item-avatar\n                      img(v-if='backer.avatar', :src='backer.avatar')\n                      v-avatar(v-else, color='blue-grey', size='40')\n                        span.white--text.subtitle-1 {{backer.name[0].toUpperCase()}}\n                    v-list-item-content\n                      v-list-item-title {{backer.name}}\n                      v-list-item-subtitle: .caption Since {{backer.joined | moment('MMMM DD, YYYY')}} on {{backer.source}}\n                    v-list-item-action(v-if='backer.twitter')\n                      v-btn(icon, :href='backer.twitter', target='_blank')\n                        v-icon(color='grey') mdi-twitter\n                    v-list-item-action(v-if='backer.website')\n                      v-btn(icon, :href='backer.website', target='_blank')\n                        v-icon(color='grey') mdi-earth\n          v-toolbar(color='primary', dense, dark)\n            .subtitle-1 Special Thanks\n          v-list(two-line)\n            v-list-item\n              v-list-item-avatar\n                img(src='https://static.requarks.io/logo/algolia.svg', alt='Algolia')\n              v-list-item-content\n                v-list-item-title Algolia\n                v-list-item-subtitle Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations.\n              v-list-item-action\n                v-btn(icon, href='https://www.algolia.com/', target='_blank')\n                  v-icon(color='grey') mdi-earth\n            v-divider\n            v-list-item\n              v-list-item-avatar\n                img(src='https://static.requarks.io/logo/browserstack.svg', alt='Browserstack')\n              v-list-item-content\n                v-list-item-title BrowserStack\n                v-list-item-subtitle BrowserStack is a cloud web and mobile testing platform that enables developers to test their websites and mobile applications.\n              v-list-item-action\n                v-btn(icon, href='https://www.browserstack.com/', target='_blank')\n                  v-icon(color='grey') mdi-earth\n            v-divider\n            v-list-item\n              v-list-item-avatar\n                img(src='https://static.requarks.io/logo/cloudflare.svg', alt='Cloudflare')\n              v-list-item-content\n                v-list-item-title Cloudflare\n                v-list-item-subtitle Providing content delivery network services, DDoS mitigation, Internet security and distributed domain name server services.\n              v-list-item-action\n                v-btn(icon, href='https://www.cloudflare.com/', target='_blank')\n                  v-icon(color='grey') mdi-earth\n            v-divider\n            v-list-item\n              v-list-item-avatar\n                img(src='https://static.requarks.io/logo/digitalocean.svg', alt='DigitalOcean')\n              v-list-item-content\n                v-list-item-title DigitalOcean\n                v-list-item-subtitle Providing developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces), and more.\n              v-list-item-action\n                v-btn(icon, href='https://m.do.co/c/5f7445bfa4d0', target='_blank')\n                  v-icon(color='grey') mdi-earth\n            v-divider\n            v-list-item\n              v-list-item-avatar(tile)\n                img(src='/_assets/svg/logo-icons8.svg', alt='Icons8')\n              v-list-item-content\n                v-list-item-title Icons8\n                v-list-item-subtitle All the Icons You Need. Guaranteed.\n              v-list-item-action\n                v-btn(icon, href='https://icons8.com', target='_blank')\n                  v-icon(color='grey') mdi-earth\n            v-divider\n            v-list-item\n              v-list-item-avatar(tile)\n                img(src='https://static.requarks.io/logo/lokalise.png', alt='Lokalise')\n              v-list-item-content\n                v-list-item-title Lokalise\n                v-list-item-subtitle Lokalise is a translation management system built for agile teams who want to automate their localization process.\n              v-list-item-action\n                v-btn(icon, href='https://lokalise.co', target='_blank')\n                  v-icon(color='grey') mdi-earth\n            v-divider\n            v-list-item\n              v-list-item-avatar(tile)\n                img(src='https://static.requarks.io/logo/netlify.svg', alt='Netlify')\n              v-list-item-content\n                v-list-item-title Netlify\n                v-list-item-subtitle Deploy modern static websites with Netlify. Get CDN, Continuous deployment, 1-click HTTPS, and all the services you need.\n              v-list-item-action\n                v-btn(icon, href='https://www.netlify.com', target='_blank')\n                  v-icon(color='grey') mdi-earth\n\n</template>\n\n<script>\nimport gql from 'graphql-tag'\n\nexport default {\n  data() {\n    return {\n      backers: []\n    }\n  },\n  apollo: {\n    backers: {\n      query: gql`\n        {\n          contribute {\n            contributors {\n              id\n              source\n              name\n              joined\n              website\n              twitter\n              avatar\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.contribute.contributors,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-contribute-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.admin-contribute {\n\n  &-tabs {\n    .v-tabs__item img {\n      height: 24px;\n      margin-bottom: 5px;\n    }\n  }\n\n  &-ethaddress {\n    display: inline-block;\n    margin-bottom: 12px;\n    border-radius: 7px;\n    background-color: mc('grey', '100');\n    color: mc('grey', '700');\n    padding: 12px;\n\n    strong {\n      display: block;\n    }\n  }\n\n  ul {\n    margin-left: 1rem;\n    list-style-type: square;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-dashboard.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-browse-page.svg', alt='Dashboard', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:dashboard.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:dashboard.subtitle') }}\n      v-flex(xs12 md6 lg4 xl3 d-flex)\n        v-card.primary.dashboard-card.animated.fadeInUp(dark)\n          v-card-text\n            v-icon.dashboard-icon mdi-file-document-outline\n            .overline {{$t('admin:dashboard.pages')}}\n            animated-number.display-1(\n              :value='info.pagesTotal'\n              :duration='2000'\n              :formatValue='round'\n              easing='easeOutQuint'\n              )\n      v-flex(xs12 md6 lg4 xl3 d-flex)\n        v-card.blue.darken-3.dashboard-card.animated.fadeInUp.wait-p2s(dark)\n          v-card-text\n            v-icon.dashboard-icon mdi-account\n            .overline {{$t('admin:dashboard.users')}}\n            animated-number.display-1(\n              :value='info.usersTotal'\n              :duration='2000'\n              :formatValue='round'\n              easing='easeOutQuint'\n              )\n      v-flex(xs12 md6 lg4 xl3 d-flex)\n        v-card.blue.darken-4.dashboard-card.animated.fadeInUp.wait-p4s(dark)\n          v-card-text\n            v-icon.dashboard-icon mdi-account-group\n            .overline {{$t('admin:dashboard.groups')}}\n            animated-number.display-1(\n              :value='info.groupsTotal'\n              :duration='2000'\n              :formatValue='round'\n              easing='easeOutQuint'\n              )\n      v-flex(xs12 md6 lg12 xl3 d-flex)\n        v-card.dashboard-card.animated.fadeInUp.wait-p6s(\n          :class='isLatestVersion ? \"green\" : \"red lighten-2\"'\n          dark\n          )\n          v-btn.btn-animate-wrench(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, to='system', v-if='hasPermission(`manage:system`)')\n            v-icon(:color='isLatestVersion ? `green` : `red darken-4`', small) mdi-wrench\n          v-card-text\n            v-icon.dashboard-icon mdi-blur\n            .subtitle-1 Wiki.js {{info.currentVersion}}\n            .body-2(v-if='isLatestVersion') {{$t('admin:dashboard.versionLatest')}}\n            .body-2(v-else) {{$t('admin:dashboard.versionNew', { version: info.latestVersion })}}\n      v-flex(xs12, xl6)\n        v-card.radius-7.animated.fadeInUp.wait-p2s\n          v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)\n            v-spacer\n            .overline {{$t('admin:dashboard.recentPages')}}\n            v-spacer\n          v-data-table.pb-2(\n            :items='recentPages'\n            :headers='recentPagesHeaders'\n            :loading='recentPagesLoading'\n            hide-default-footer\n            hide-default-header\n            )\n            template(slot='item', slot-scope='props')\n              tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')\n                td\n                  .body-2: strong {{ props.item.title }}\n                td.admin-pages-path\n                  v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}\n                  span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}\n                td.text-right.caption(width='250') {{ props.item.updatedAt | moment('calendar') }}\n      v-flex(xs12, xl6)\n        v-card.radius-7.animated.fadeInUp.wait-p4s\n          v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)\n            v-spacer\n            .overline {{$t('admin:dashboard.lastLogins')}}\n            v-spacer\n          v-data-table.pb-2(\n            :items='lastLogins'\n            :headers='lastLoginsHeaders'\n            :loading='lastLoginsLoading'\n            hide-default-footer\n            hide-default-header\n            )\n            template(slot='item', slot-scope='props')\n              tr.is-clickable(:active='props.selected', @click='$router.push(`/users/` + props.item.id)')\n                td\n                  .body-2: strong {{ props.item.name }}\n                td.text-right.caption(width='250') {{ props.item.lastLoginAt | moment('calendar') }}\n\n      v-flex(xs12)\n        v-card.dashboard-contribute.animated.fadeInUp.wait-p4s\n          v-card-text\n            img(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='height: 80px;')\n            .pl-5\n              .subtitle-1 {{$t('admin:contribute.title')}}\n              .body-2.mt-3: strong {{$t('admin:dashboard.contributeSubtitle')}}\n              .body-2 {{$t('admin:dashboard.contributeHelp')}}\n              v-btn.mx-0.mt-4(:color='$vuetify.theme.dark ? `indigo lighten-3` : `indigo`', outlined, small, to='/contribute')\n                .caption: strong {{$t('admin:dashboard.contributeLearnMore')}}\n\n</template>\n\n<script>\nimport _ from 'lodash'\nimport AnimatedNumber from 'animated-number-vue'\nimport { get } from 'vuex-pathify'\nimport gql from 'graphql-tag'\nimport semverLte from 'semver/functions/lte'\n\nexport default {\n  components: {\n    AnimatedNumber\n  },\n  data() {\n    return {\n      recentPages: [],\n      recentPagesLoading: false,\n      recentPagesHeaders: [\n        { text: 'Title', value: 'title' },\n        { text: 'Path', value: 'path' },\n        { text: 'Last Updated', value: 'updatedAt', width: 250 }\n      ],\n      lastLogins: [],\n      lastLoginsLoading: false,\n      lastLoginsHeaders: [\n        { text: 'User', value: 'displayName' },\n        { text: 'Last Login', value: 'lastLoginAt', width: 250 }\n      ]\n    }\n  },\n  computed: {\n    isLatestVersion() {\n      if (this.info.latestVersion === 'n/a' || this.info.currentVersion === 'n/a') {\n        return true\n      } else {\n        return semverLte(this.info.latestVersion, this.info.currentVersion)\n      }\n    },\n    info: get('admin/info'),\n    permissions: get('user/permissions')\n  },\n  methods: {\n    round(val) { return Math.round(val) },\n    hasPermission(prm) {\n      if (_.isArray(prm)) {\n        return _.some(prm, p => {\n          return _.includes(this.permissions, p)\n        })\n      } else {\n        return _.includes(this.permissions, prm)\n      }\n    }\n  },\n  apollo: {\n    recentPages: {\n      query: gql`\n        query {\n          pages {\n            list(limit: 10, orderBy: UPDATED, orderByDirection: DESC) {\n              id\n              locale\n              path\n              title\n              description\n              contentType\n              isPublished\n              isPrivate\n              privateNS\n              createdAt\n              updatedAt\n            }\n          }\n        }\n      `,\n      update: (data) => data.pages.list,\n      watchLoading (isLoading) {\n        this.recentPagesLoading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-recentpages')\n      }\n    },\n    lastLogins: {\n      query: gql`\n        query {\n          users {\n            lastLogins {\n              id\n              name\n              lastLoginAt\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.users.lastLogins,\n      watchLoading (isLoading) {\n        this.lastLoginsLoading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-lastlogins')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.dashboard-card {\n  display: flex;\n  width: 100%;\n  border-radius: 7px;\n\n  .v-card__text {\n    overflow: hidden;\n    position: relative;\n  }\n}\n\n.dashboard-contribute {\n  background-color: #FFF;\n  background-image: linear-gradient(to bottom, #FFF 0%, lighten(mc('indigo', '50'), 3%) 100%);\n  border-radius: 7px;\n\n  @at-root .theme--dark & {\n    background-color: mc('grey', '800');\n    background-image: linear-gradient(to bottom, mc('grey', '800') 0%, darken(mc('grey', '800'), 6%) 100%);\n  }\n\n  .v-card__text {\n    display: flex;\n    align-items: center;\n    color: mc('indigo', '500') !important;\n\n    @at-root .theme--dark & {\n      color: mc('grey', '300') !important;\n    }\n  }\n}\n\n.v-icon.dashboard-icon {\n  position: absolute !important;\n  right: 0;\n  top: 12px;\n  font-size: 100px !important;\n  opacity: .25;\n\n  @at-root .v-application--is-rtl & {\n    left: 0;\n    right: initial;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-dev-flags.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img(src='/_assets/svg/icon-console.svg', alt='Developer Tools', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text Developer Tools\n            .subtitle-1.grey--text Flags\n          v-spacer\n          v-btn(color='success', depressed, @click='save', large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n        v-card.mt-3(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `white grey--text text--darken-3`')\n          v-alert(color='red', :value='true', icon='mdi-alert', dark, prominent)\n            span Do NOT enable these flags unless you know what you're doing!\n            .caption Doing so may result in data loss or broken installation!\n          v-card-text\n            v-switch.mt-3(\n              color='primary'\n              hint='Log detailed debug info on LDAP/AD login attempts.'\n              persistent-hint\n              label='LDAP Debug'\n              v-model='flags.ldapdebug'\n              inset\n            )\n            v-divider.mt-3\n            v-switch.mt-3(\n              color='red'\n              hint='Log all queries made to the database to console.'\n              persistent-hint\n              label='SQL Query Logging'\n              v-model='flags.sqllog'\n              inset\n            )\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport flagsQuery from 'gql/admin/dev/dev-query-flags.gql'\nimport flagsMutation from 'gql/admin/dev/dev-mutation-save-flags.gql'\n\nexport default {\n  data() {\n    return {\n      flags: {\n        sqllog: false\n      }\n    }\n  },\n  methods: {\n    async save() {\n      try {\n        await this.$apollo.mutate({\n          mutation: flagsMutation,\n          variables: {\n            flags: _.transform(this.flags, (result, value, key) => {\n              result.push({ key, value })\n            }, [])\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dev-flags-update')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: 'Flags applied successfully.',\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    }\n  },\n  apollo: {\n    flags: {\n      query: flagsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.transform(data.system.flags, (result, row) => {\n        _.set(result, row.key, row.value)\n      }, {}),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dev-flags-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-editor.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img(src='/_assets/svg/icon-web-design.svg', alt='Editor', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text Editor\n            .subtitle-1.grey--text Configure the content editors #[v-chip(label, color='primary', small).white--text coming soon]\n          v-spacer\n          v-btn(outline, color='grey', @click='refresh', large)\n            v-icon refresh\n          v-btn(color='success', @click='save', depressed, large)\n            v-icon(left) check\n            span {{$t('common:actions.apply')}}\n\n        v-card.mt-3\n          v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark)\n            v-tab(key='settings'): v-icon settings\n            v-tab(key='code') Markdown\n\n            v-tab-item(key='settings', :transition='false', :reverse-transition='false')\n              v-card.pa-3(flat, tile)\n                .body-2.grey--text.text--darken-1 Select which editors to enable:\n                .caption.grey--text.pb-2 Some editors require additional configuration in their dedicated tab (when selected).\n                v-form\n                  v-checkbox.my-0(\n                    v-for='editor in editors'\n                    v-model='editor.isEnabled'\n                    :key='editor.key'\n                    :label='editor.title'\n                    color='primary'\n                    disabled\n                    hide-details\n                  )\n            v-tab-item(key='code', :transition='false', :reverse-transition='false')\n              v-card.wiki-form.pa-3(flat, tile)\n                v-form\n                  v-subheader Editor Configuration\n                  .body-1.ml-3 This editor has no configuration options you can modify.\n</template>\n\n<script>\nexport default {\n  data() {\n    return {\n      editors: [\n        { title: 'API Docs', key: 'api', isEnabled: false },\n        { title: 'Code', key: 'code', isEnabled: true },\n        { title: 'Markdown', key: 'markdown', isEnabled: true },\n        { title: 'Tabular', key: 'tabular', isEnabled: false },\n        { title: 'Visual Builder', key: 'visual', isEnabled: false },\n        { title: 'WikiText', key: 'wikitext', isEnabled: false }\n      ]\n    }\n  },\n  methods: {\n    save() {},\n    refresh() {}\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-extensions.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-installing-updates.svg', alt='Extensions', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:extensions.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:extensions.subtitle') }}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(xl6 lg8 xs12)\n              v-alert.mb-4(outlined, color='error', icon='mdi-alert')\n                span New extensions cannot be installed at the moment. This feature is coming in a future release.\n              v-expansion-panels.admin-extensions-exp(hover, popout)\n                v-expansion-panel(v-for='ext of extensions', :key='`ext-` + ext.key')\n                  v-expansion-panel-header(disable-icon-rotate)\n                    span {{ext.title}}\n                    template(v-slot:actions)\n                      v-chip(label, color='success', small, v-if='ext.isInstalled') Installed\n                      v-chip(label, color='warning', small, v-else) Not Installed\n                  v-expansion-panel-content.pa-0\n                    v-card(flat, :class='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-5`', tile)\n                      v-card-text\n                        .body-2 {{ext.description}}\n                        v-divider.my-4\n                        .body-2\n                          strong.mr-2 This extension is\n                          v-chip.mr-2(v-if='ext.isCompatible', label, outlined, small, color='success') compatible\n                          v-chip.mr-2(v-else, label, small, color='error') not compatible\n                          strong with your host.\n                      v-card-chin\n                        v-spacer\n                        v-btn(disabled)\n                          v-icon(left) mdi-plus\n                          span Install\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nexport default {\n  data() {\n    return {\n      extensions: []\n    }\n  },\n  methods: {\n    async save () {\n      // try {\n      //   await this.$apollo.mutate({\n      //     mutation: gql`\n      //       mutation (\n      //         $host: String!\n      //       ) {\n      //         site {\n      //           updateConfig(\n      //             host: $host\n      //           ) {\n      //             responseResult {\n      //               succeeded\n      //               errorCode\n      //               slug\n      //               message\n      //             }\n      //           }\n      //         }\n      //       }\n      //     `,\n      //     variables: {\n      //       host: _.get(this.config, 'host', '')\n      //     },\n      //     watchLoading (isLoading) {\n      //       this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-extensions-update')\n      //     }\n      //   })\n      //   this.$store.commit('showNotification', {\n      //     style: 'success',\n      //     message: 'Configuration saved successfully.',\n      //     icon: 'check'\n      //   })\n      // } catch (err) {\n      //   this.$store.commit('pushGraphError', err)\n      // }\n    }\n  },\n  apollo: {\n    extensions: {\n      query: gql`\n        {\n          system {\n            extensions {\n              key\n              title\n              description\n              isInstalled\n              isCompatible\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.system.extensions),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-extensions-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.admin-extensions-exp {\n  .v-expansion-panel-content__wrap {\n    padding: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-general.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-categorize.svg', alt='General', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:general.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:general.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(lg6 xs12)\n              v-form\n                v-card.animated.fadeInUp\n                  v-toolbar(color='primary', dark, dense, flat)\n                    v-toolbar-title.subtitle-1 {{ $t('admin:general.siteInfo') }}\n                  .overline.grey--text.pa-4 {{$t('admin:general.general')}}\n                  .px-3.pb-3\n                    v-text-field(\n                      outlined\n                      :label='$t(`admin:general.siteUrl`)'\n                      required\n                      :counter='255'\n                      v-model='config.host'\n                      prepend-icon='mdi-label-variant-outline'\n                      :hint='$t(`admin:general.siteUrlHint`)'\n                      persistent-hint\n                      )\n                    v-text-field.mt-3(\n                      outlined\n                      :label='$t(`admin:general.siteTitle`)'\n                      required\n                      :counter='50'\n                      v-model='config.title'\n                      prepend-icon='mdi-earth'\n                      :hint='$t(`admin:general.siteTitleHint`)'\n                      persistent-hint\n                      )\n                  v-divider\n                  .overline.grey--text.pa-4 {{$t('admin:general.logo')}}\n                  .pt-2.pb-7.pl-10.pr-3\n                    .d-flex.align-center\n                      v-avatar(size='100', tile)\n                        v-img(\n                          :src='config.logoUrl'\n                          lazy-src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNcWQ8AAdcBKrJda2oAAAAASUVORK5CYII='\n                          aspect-ratio='1'\n                          )\n                      .ml-4(style='flex: 1 1 auto;')\n                        v-text-field(\n                          outlined\n                          :label='$t(`admin:general.logoUrl`)'\n                          v-model='config.logoUrl'\n                          :hint='$t(`admin:general.logoUrlHint`)'\n                          persistent-hint\n                          append-icon='mdi-folder-image'\n                          @click:append='browseLogo'\n                          @keyup.enter='refreshLogo'\n                        )\n                  v-divider\n                  .overline.grey--text.pa-4 {{$t('admin:general.footerCopyright')}}\n                  .px-3.pb-3\n                    v-text-field(\n                      outlined\n                      :label='$t(`admin:general.companyName`)'\n                      v-model='config.company'\n                      :counter='255'\n                      prepend-icon='mdi-domain'\n                      persistent-hint\n                      :hint='$t(`admin:general.companyNameHint`)'\n                      )\n                    v-select.mt-3(\n                      outlined\n                      :label='$t(`admin:general.contentLicense`)'\n                      :items='contentLicenses'\n                      v-model='config.contentLicense'\n                      prepend-icon='mdi-creative-commons'\n                      :return-object='false'\n                      :hint='$t(`admin:general.contentLicenseHint`)'\n                      persistent-hint\n                    )\n                    v-text-field.mt-3(\n                      outlined\n                      :label='$t(`admin:general.footerOverride`)'\n                      v-model='config.footerOverride'\n                      prepend-icon='mdi-page-layout-footer'\n                      append-icon='mdi-language-markdown'\n                      persistent-hint\n                      :hint='$t(`admin:general.footerOverrideHint`)'\n                      )\n                  v-divider\n                  .overline.grey--text.pa-4 SEO\n                  .px-3.pb-3\n                    v-text-field(\n                      outlined\n                      :label='$t(`admin:general.siteDescription`)'\n                      :counter='255'\n                      v-model='config.description'\n                      prepend-icon='mdi-compass'\n                      :hint='$t(`admin:general.siteDescriptionHint`)'\n                      persistent-hint\n                      )\n                    v-select.mt-3(\n                      outlined\n                      :label='$t(`admin:general.metaRobots`)'\n                      multiple\n                      :items='metaRobots'\n                      v-model='config.robots'\n                      prepend-icon='mdi-compass'\n                      :return-object='false'\n                      :hint='$t(`admin:general.metaRobotsHint`)'\n                      persistent-hint\n                      )\n\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp.wait-p4s\n                v-toolbar(color='indigo', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 Features\n                v-card-text\n                  //- v-switch(\n                  //-   inset\n                  //-   label='Asset Image Optimization'\n                  //-   color='indigo'\n                  //-   v-model='config.featureTinyPNG'\n                  //-   persistent-hint\n                  //-   hint='Image optimization tool to reduce filesize and bandwidth costs.'\n                  //-   disabled\n                  //-   )\n                  //- v-text-field.mt-3(\n                  //-   outlined\n                  //-   label='TinyPNG API Key'\n                  //-   :counter='255'\n                  //-   v-model='config.description'\n                  //-   prepend-icon='mdi-subdirectory-arrow-right'\n                  //-   hint='Get your API key at https://tinypng.com/developers'\n                  //-   persistent-hint\n                  //-   disabled\n                  //-   )\n\n                  //- v-divider.mt-3\n                  //- v-switch(\n                  //-   inset\n                  //-   label='Page Ratings'\n                  //-   color='indigo'\n                  //-   v-model='config.featurePageRatings'\n                  //-   persistent-hint\n                  //-   hint='Allow users to rate pages.'\n                  //-   disabled\n                  //-   )\n\n                  //- v-divider.mt-3\n                  v-switch.mt-0(\n                    inset\n                    label='Comments'\n                    color='indigo'\n                    v-model='config.featurePageComments'\n                    persistent-hint\n                    hint='Allow users to leave comments on pages.'\n                    )\n\n                  //- v-divider.mt-3\n                  //- v-switch(\n                  //-   inset\n                  //-   label='Personal Wikis'\n                  //-   color='indigo'\n                  //-   v-model='config.featurePersonalWikis'\n                  //-   persistent-hint\n                  //-   hint='Allow users to have their own personal wiki.'\n                  //-   disabled\n                  //-   )\n\n              v-card.mt-5.animated.fadeInUp.wait-p6s\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 URL Handling\n                v-card-text\n                  v-text-field(\n                    outlined\n                    :label='$t(`admin:general.pageExtensions`)'\n                    v-model='config.pageExtensions'\n                    prepend-icon='mdi-format-text-wrapping-overflow'\n                    :hint='$t(`admin:general.pageExtensionsHint`)'\n                    persistent-hint\n                    )\n\n              v-card.mt-5.animated.fadeInUp.wait-p7s\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{$t('admin:general.editShortcuts')}}\n                v-card-text\n                  v-switch.mt-0(\n                    inset\n                    :label='$t(`admin:general.editFab`)'\n                    color='primary'\n                    v-model='config.editFab'\n                    persistent-hint\n                    :hint='$t(`admin:general.editFabHint`)'\n                    )\n                v-divider\n                .overline.grey--text.pa-4 {{$t('admin:general.editMenuBar')}}\n                .px-3.pb-3\n                  v-switch.mt-0.ml-1(\n                    inset\n                    :label='$t(`admin:general.displayEditMenuBar`)'\n                    color='primary'\n                    v-model='config.editMenuBar'\n                    persistent-hint\n                    :hint='$t(`admin:general.displayEditMenuBarHint`)'\n                    )\n                  v-switch.mt-4.ml-1(\n                    v-if='config.editMenuBar'\n                    inset\n                    :label='$t(`admin:general.displayEditMenuBtn`)'\n                    color='primary'\n                    v-model='config.editMenuBtn'\n                    persistent-hint\n                    :hint='$t(`admin:general.displayEditMenuBtnHint`)'\n                    )\n                  v-switch.mt-4.ml-1(\n                    v-if='config.editMenuBar'\n                    inset\n                    :label='$t(`admin:general.displayEditMenuExternalBtn`)'\n                    color='primary'\n                    v-model='config.editMenuExternalBtn'\n                    persistent-hint\n                    :hint='$t(`admin:general.displayEditMenuExternalBtnHint`)'\n                    )\n                template(v-if='config.editMenuBar && config.editMenuExternalBtn')\n                  v-divider\n                  .overline.grey--text.pa-4 External Edit Button\n                  .px-3.pb-3\n                    v-text-field(\n                      outlined\n                      :label='$t(`admin:general.editMenuExternalName`)'\n                      v-model='config.editMenuExternalName'\n                      prepend-icon='mdi-format-title'\n                      :hint='$t(`admin:general.editMenuExternalNameHint`)'\n                      persistent-hint\n                      )\n                    v-text-field.mt-3(\n                      outlined\n                      :label='$t(`admin:general.editMenuExternalIcon`)'\n                      v-model='config.editMenuExternalIcon'\n                      prepend-icon='mdi-dice-5'\n                      :hint='$t(`admin:general.editMenuExternalIconHint`)'\n                      persistent-hint\n                      )\n                    v-text-field.mt-3(\n                      outlined\n                      :label='$t(`admin:general.editMenuExternalUrl`)'\n                      v-model='config.editMenuExternalUrl'\n                      prepend-icon='mdi-near-me'\n                      :hint='$t(`admin:general.editMenuExternalUrlHint`)'\n                      persistent-hint\n                      )\n\n    component(:is='activeModal')\n\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync } from 'vuex-pathify'\nimport gql from 'graphql-tag'\n\nimport editorStore from '../../store/editor'\n\n/* global WIKI */\n\nconst titleRegex = /[<>\"]/i\n\nWIKI.$store.registerModule('editor', editorStore)\n\nexport default {\n  i18nOptions: { namespaces: 'editor' },\n  components: {\n    editorModalMedia: () => import(/* webpackChunkName: \"editor\", webpackMode: \"lazy\" */ '../editor/editor-modal-media.vue')\n  },\n  data() {\n    return {\n      config: {\n        host: '',\n        title: '',\n        description: '',\n        robots: [],\n        analyticsService: '',\n        analyticsId: '',\n        company: '',\n        contentLicense: '',\n        footerOverride: '',\n        logoUrl: '',\n        featureAnalytics: false,\n        featurePageRatings: false,\n        featurePageComments: false,\n        featurePersonalWikis: false,\n        featureTinyPNG: false,\n        pageExtensions: '',\n        editFab: false,\n        editMenuBar: false,\n        editMenuBtn: false,\n        editMenuExternalBtn: false,\n        editMenuExternalName: '',\n        editMenuExternalIcon: '',\n        editMenuExternalUrl: ''\n      },\n      metaRobots: [\n        { text: 'Index', value: 'index' },\n        { text: 'Follow', value: 'follow' },\n        { text: 'No Index', value: 'noindex' },\n        { text: 'No Follow', value: 'nofollow' }\n      ]\n    }\n  },\n  computed: {\n    siteTitle: sync('site/title'),\n    logoUrl: sync('site/logoUrl'),\n    company: sync('site/company'),\n    contentLicense: sync('site/contentLicense'),\n    footerOverride: sync('site/footerOverride'),\n    activeModal: sync('editor/activeModal'),\n    contentLicenses () {\n      return [\n        { value: '', text: this.$t('common:license.none') },\n        { value: 'alr', text: this.$t('common:license.alr') },\n        { value: 'cc0', text: this.$t('common:license.cc0') },\n        { value: 'ccby', text: this.$t('common:license.ccby') },\n        { value: 'ccbysa', text: this.$t('common:license.ccbysa') },\n        { value: 'ccbynd', text: this.$t('common:license.ccbynd') },\n        { value: 'ccbync', text: this.$t('common:license.ccbync') },\n        { value: 'ccbyncsa', text: this.$t('common:license.ccbyncsa') },\n        { value: 'ccbyncnd', text: this.$t('common:license.ccbyncnd') }\n      ]\n    }\n  },\n  methods: {\n    async save () {\n      const title = _.get(this.config, 'title', '')\n      if (titleRegex.test(title)) {\n        this.$store.commit('showNotification', {\n          style: 'error',\n          message: this.$t('admin:general.siteTitleInvalidChars'),\n          icon: 'alert'\n        })\n        return\n      }\n      try {\n        await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $host: String\n              $title: String\n              $description: String\n              $robots: [String]\n              $analyticsService: String\n              $analyticsId: String\n              $company: String\n              $contentLicense: String\n              $footerOverride: String\n              $logoUrl: String\n              $pageExtensions: String\n              $featurePageRatings: Boolean\n              $featurePageComments: Boolean\n              $featurePersonalWikis: Boolean\n              $editFab: Boolean\n              $editMenuBar: Boolean\n              $editMenuBtn: Boolean\n              $editMenuExternalBtn: Boolean\n              $editMenuExternalName: String\n              $editMenuExternalIcon: String\n              $editMenuExternalUrl: String\n            ) {\n              site {\n                updateConfig(\n                  host: $host\n                  title: $title\n                  description: $description\n                  robots: $robots\n                  analyticsService: $analyticsService\n                  analyticsId: $analyticsId\n                  company: $company\n                  contentLicense: $contentLicense\n                  footerOverride: $footerOverride\n                  logoUrl: $logoUrl\n                  pageExtensions: $pageExtensions\n                  featurePageRatings: $featurePageRatings\n                  featurePageComments: $featurePageComments\n                  featurePersonalWikis: $featurePersonalWikis\n                  editFab: $editFab\n                  editMenuBar: $editMenuBar\n                  editMenuBtn: $editMenuBtn\n                  editMenuExternalBtn: $editMenuExternalBtn\n                  editMenuExternalName: $editMenuExternalName\n                  editMenuExternalIcon: $editMenuExternalIcon\n                  editMenuExternalUrl: $editMenuExternalUrl\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            host: _.get(this.config, 'host', ''),\n            title: _.get(this.config, 'title', ''),\n            description: _.get(this.config, 'description', ''),\n            robots: _.get(this.config, 'robots', []),\n            analyticsService: _.get(this.config, 'analyticsService', ''),\n            analyticsId: _.get(this.config, 'analyticsId', ''),\n            company: _.get(this.config, 'company', ''),\n            contentLicense: _.get(this.config, 'contentLicense', ''),\n            footerOverride: _.get(this.config, 'footerOverride', ''),\n            logoUrl: _.get(this.config, 'logoUrl', ''),\n            pageExtensions: _.get(this.config, 'pageExtensions', ''),\n            featurePageRatings: _.get(this.config, 'featurePageRatings', false),\n            featurePageComments: _.get(this.config, 'featurePageComments', false),\n            featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false),\n            editFab: _.get(this.config, 'editFab', false),\n            editMenuBar: _.get(this.config, 'editMenuBar', false),\n            editMenuBtn: _.get(this.config, 'editMenuBtn', false),\n            editMenuExternalBtn: _.get(this.config, 'editMenuExternalBtn', false),\n            editMenuExternalName: _.get(this.config, 'editMenuExternalName', ''),\n            editMenuExternalIcon: _.get(this.config, 'editMenuExternalIcon', ''),\n            editMenuExternalUrl: _.get(this.config, 'editMenuExternalUrl', '')\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:general.saveSuccess'),\n          icon: 'check'\n        })\n        this.siteTitle = this.config.title\n        this.company = this.config.company\n        this.contentLicense = this.config.contentLicense\n        this.footerOverride = this.config.footerOverride\n        this.logoUrl = this.config.logoUrl\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    },\n    browseLogo () {\n      this.$store.set('editor/editorKey', 'common')\n      this.activeModal = 'editorModalMedia'\n    },\n    refreshLogo () {\n      this.$forceUpdate()\n    }\n  },\n  mounted () {\n    this.$root.$on('editorInsert', opts => {\n      this.config.logoUrl = opts.path\n    })\n  },\n  beforeDestroy() {\n    this.$root.$off('editorInsert')\n  },\n  apollo: {\n    config: {\n      query: gql`\n        {\n          site {\n            config {\n              host\n              title\n              description\n              robots\n              analyticsService\n              analyticsId\n              company\n              contentLicense\n              footerOverride\n              logoUrl\n              pageExtensions\n              featurePageRatings\n              featurePageComments\n              featurePersonalWikis\n              editFab\n              editMenuBar\n              editMenuBtn\n              editMenuExternalBtn\n              editMenuExternalName\n              editMenuExternalIcon\n              editMenuExternalUrl\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.site.config),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-groups-edit-permissions.vue",
    "content": "<template lang=\"pug\">\n  v-card(flat)\n    v-container.px-3.pb-3.pt-3(fluid, grid-list-md)\n      v-layout(row, wrap)\n        v-flex(xs12, v-if='group.isSystem')\n          v-alert.radius-7.mb-0(\n            color='orange darken-2'\n            :class='$vuetify.theme.dark ? \"grey darken-4\" : \"orange lighten-5\"'\n            outlined\n            :value='true'\n            icon='mdi-lock-outline'\n            ) This is a system group. Some permissions cannot be modified.\n        v-flex(xs12, md6, lg4, v-for='pmGroup in permissions', :key='pmGroup.category')\n          v-card.md2(flat, :class='$vuetify.theme.dark ? \"grey darken-3-d5\" : \"grey lighten-5\"')\n            .overline.px-5.pt-5.pb-3.grey--text.text--darken-2 {{pmGroup.category}}\n            v-card-text.pt-0\n              template(v-for='(pm, idx) in pmGroup.items')\n                v-checkbox.pt-0(\n                  style='justify-content: space-between;'\n                  :key='pm.permission'\n                  :label='pm.permission'\n                  :hint='pm.hint'\n                  persistent-hint\n                  color='primary'\n                  v-model='group.permissions'\n                  :value='pm.permission'\n                  :append-icon='pm.warning ? \"mdi-alert\" : null',\n                  :disabled='(group.isSystem && pm.restrictedForSystem) || group.id === 1 || pm.disabled'\n                )\n                v-divider.mt-3(v-if='idx < pmGroup.items.length - 1')\n</template>\n\n<script>\nexport default {\n  props: {\n    value: {\n      type: Object,\n      default: () => ({})\n    }\n  },\n  data() {\n    return {\n      permissions: [\n        {\n          category: 'Content',\n          items: [\n            {\n              permission: 'read:pages',\n              hint: 'Can view pages, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: false,\n              disabled: false\n            },\n            {\n              permission: 'write:pages',\n              hint: 'Can create / edit pages, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:pages',\n              hint: 'Can move existing pages as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'delete:pages',\n              hint: 'Can delete existing pages, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'write:styles',\n              hint: 'Can insert CSS styles in pages, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'write:scripts',\n              hint: 'Can insert JavaScript in pages, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'read:source',\n              hint: 'Can view pages source, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: false,\n              disabled: false\n            },\n            {\n              permission: 'read:history',\n              hint: 'Can view pages history, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: false,\n              disabled: false\n            },\n            {\n              permission: 'read:assets',\n              hint: 'Can view / use assets (such as images and files), as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: false,\n              disabled: false\n            },\n            {\n              permission: 'write:assets',\n              hint: 'Can upload new assets (such as images and files), as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:assets',\n              hint: 'Can edit and delete existing assets (such as images and files), as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'read:comments',\n              hint: 'Can view comments, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: false,\n              disabled: false\n            },\n            {\n              permission: 'write:comments',\n              hint: 'Can post new comments, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: false,\n              disabled: false\n            },\n            {\n              permission: 'manage:comments',\n              hint: 'Can edit and delete existing comments, as specified in the Page Rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            }\n          ]\n        },\n        {\n          category: 'Users',\n          items: [\n            {\n              permission: 'write:users',\n              hint: 'Can create or authorize new users, but not modify existing ones',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:users',\n              hint: 'Can manage all users (but not users with administrative permissions)',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'write:groups',\n              hint: 'Can manage groups and assign CONTENT permissions / page rules',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:groups',\n              hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',\n              warning: true,\n              restrictedForSystem: true,\n              disabled: false\n            }\n          ]\n        },\n        {\n          category: 'Administration',\n          items: [\n            {\n              permission: 'manage:navigation',\n              hint: 'Can manage the site navigation',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:theme',\n              hint: 'Can manage and modify themes',\n              warning: false,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:api',\n              hint: 'Can generate and revoke API keys',\n              warning: true,\n              restrictedForSystem: true,\n              disabled: false\n            },\n            {\n              permission: 'manage:system',\n              hint: 'Can manage and access everything. Root administrator.',\n              warning: true,\n              restrictedForSystem: true,\n              disabled: true\n\n            }\n          ]\n        }\n      ]\n    }\n  },\n  computed: {\n    group: {\n      get() { return this.value },\n      set(val) { this.$set('input', val) }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-groups-edit-rules.vue",
    "content": "<template lang=\"pug\">\n  v-card(flat)\n    v-card-text(v-if='group.id === 1')\n      v-alert.radius-7.mb-0(\n        :class='$vuetify.theme.dark ? \"grey darken-4\" : \"orange lighten-5\"'\n        color='orange darken-2'\n        outlined\n        icon='mdi-lock-outline'\n        ) This group has access to everything.\n    template(v-else)\n      v-card-title(:class='$vuetify.theme.dark ? `grey darken-3-d5` : ``')\n        v-alert.radius-7.caption(\n          :class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-4`'\n          color='grey'\n          outlined\n          icon='mdi-information'\n          ) You must enable global content permissions (under Permissions tab) for page rules to have any effect.\n        v-spacer\n        v-btn.mx-2(depressed, color='primary', @click='addRule')\n          v-icon(left) mdi-plus\n          | Add Rule\n        v-menu(\n          right\n          offset-y\n          nudge-left='115'\n          )\n          template(v-slot:activator='{ on }')\n            v-btn.is-icon(v-on='on', outlined, color='primary')\n              v-icon mdi-dots-horizontal\n          v-list(dense)\n            v-list-item(@click='comingSoon')\n              v-list-item-avatar\n                v-icon mdi-application-import\n              v-list-item-title Load Preset\n            v-divider\n            v-list-item(@click='comingSoon')\n              v-list-item-avatar\n                v-icon mdi-application-export\n              v-list-item-title Save As Preset\n            v-divider\n            v-list-item(@click='comingSoon')\n              v-list-item-avatar\n                v-icon mdi-cloud-upload\n              v-list-item-title Import Rules\n            v-divider\n            v-list-item(@click='comingSoon')\n              v-list-item-avatar\n                v-icon mdi-cloud-download\n              v-list-item-title Export Rules\n      v-card-text(:class='$vuetify.theme.dark ? `grey darken-4-l5` : `white`')\n        .rules\n          .caption(v-if='group.pageRules.length === 0')\n            em(:class='$vuetify.theme.dark ? `grey--text` : `blue-grey--text`') This group has no page rules yet.\n          .rule(v-for='rule of group.pageRules', :key='rule.id')\n            v-btn.ma-0.radius-4.rule-deny-btn(\n              solo\n              :color='rule.deny ? \"red\" : \"green\"'\n              dark\n              @click='rule.deny = !rule.deny'\n              height='48'\n              )\n              v-icon(v-if='rule.deny') mdi-cancel\n              v-icon(v-else) mdi-check-circle\n            //- Roles\n            v-select.ml-1(\n              solo\n              :items='roles'\n              v-model='rule.roles'\n              placeholder='Select Role(s)...'\n              hide-details\n              multiple\n              chips\n              deletable-chips\n              small-chips\n              height='48px'\n              style='flex: 0 1 440px;'\n              :menu-props='{ \"maxHeight\": 500 }'\n              clearable\n              dense\n              )\n              template(slot='selection', slot-scope='{ item, index }')\n                v-chip.white--text.ml-0(v-if='index <= 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}\n                v-chip.white--text.ml-0(v-if='index === 2', small, label, :color='rule.deny ? `red lighten-2` : `green lighten-2`').caption + {{ rule.roles.length - 2 }} more\n              template(slot='item', slot-scope='props')\n                v-list-item-action(style='min-width: 30px;')\n                  v-checkbox(\n                    v-model='props.attrs.inputValue'\n                    hide-details\n                    color='primary'\n                  )\n                v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}\n                v-list-item-content\n                  v-list-item-title.body-2 {{props.item.text}}\n                v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value}}\n\n            //- Match\n            v-select.ml-1.mr-1(\n              solo\n              :items='matches'\n              v-model='rule.match'\n              placeholder='Match...'\n              hide-details\n              height='48px'\n              style='flex: 0 1 250px;'\n              dense\n              )\n              template(slot='selection', slot-scope='{ item, index }')\n                .body-2 {{item.text}}\n              template(slot='item', slot-scope='data')\n                v-list-item-avatar\n                  v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}\n                v-list-item-content\n                  v-list-item-title(v-html='data.item.text')\n            //- Locales\n            v-select.mr-1(\n              :background-color='$vuetify.theme.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'\n              solo\n              :items='locales'\n              v-model='rule.locales'\n              placeholder='Any Locale'\n              item-value='code'\n              item-text='name'\n              multiple\n              hide-details\n              height='48px'\n              dense\n              :menu-props='{ \"minWidth\": 250 }'\n              style='flex: 0 1 150px;'\n              )\n              template(slot='selection', slot-scope='{ item, index }')\n                v-chip.white--text.ml-0(v-if='rule.locales.length === 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.code.toUpperCase() }}\n                v-chip.white--text.ml-0(v-else-if='index === 0', small, label, :color='rule.deny ? `red` : `green`').caption {{ rule.locales.length }} locales\n              v-list-item(slot='prepend-item', @click='rule.locales = []')\n                v-list-item-action(style='min-width: 30px;')\n                  v-checkbox(\n                    :input-value='rule.locales.length === 0'\n                    hide-details\n                    color='primary'\n                    readonly\n                  )\n                v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-earth\n                v-list-item-content\n                  v-list-item-title.body-2 Any Locale\n              v-divider(slot='prepend-item')\n              template(slot='item', slot-scope='props')\n                v-list-item-action(style='min-width: 30px;')\n                  v-checkbox(\n                    v-model='props.attrs.inputValue'\n                    hide-details\n                    color='primary'\n                  )\n                v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-web\n                v-list-item-content\n                  v-list-item-title.body-2 {{props.item.name}}\n                v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.code.toUpperCase()}}\n\n            //- Path\n            v-text-field(\n              solo\n              v-model='rule.path'\n              label='Path'\n              :prefix='(rule.match !== `END` && rule.match !== `TAG`) ? `/` : null'\n              :placeholder='rule.match === `REGEX` ? `Regular Expression` : rule.match === `TAG` ? `Tag` : `Path`'\n              :suffix='rule.match === `REGEX` ? `/` : null'\n              hide-details\n              :color='$vuetify.theme.dark ? `grey` : `blue-grey`'\n              )\n\n            v-btn.ml-2(icon, @click='removeRule(rule.id)', small)\n              v-icon(:color='$vuetify.theme.dark ? `grey` : `blue-grey`') mdi-close\n\n        v-divider.mt-3\n        .overline.py-3 Rules Order\n        .body-2.pl-3 Rules are applied in order of path specificity. A more precise path will always override a less defined path.\n        .body-2.pl-5 For example, #[span.teal--text /geography/countries] will override #[span.teal--text /geography].\n        .body-2.pl-3.pt-2 When 2 rules have the same specificity, the priority is given from lowest to highest as follows:\n        .body-2.pl-3.pt-1\n          ul\n            li\n              strong Path Starts With...\n              em.caption.pl-1 (lowest)\n            li\n              strong Path Ends With...\n            li\n              strong Path Matches Regex...\n            li\n              strong Tag Matches...\n            li\n              strong Path Is Exactly...\n              em.caption.pl-1 (highest)\n        .body-2.pl-3.pt-2 When 2 rules have the same path specificity AND the same match type, #[strong.red--text DENY] will always override an #[strong.green--text ALLOW] rule.\n        v-divider.mt-3\n        .overline.py-3 Regular Expressions\n        span Expressions that are deemed unsafe or could result in exponential time processing will be rejected upon saving.\n\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { customAlphabet } from 'nanoid/non-secure'\n\n/* global siteLangs */\n\nconst nanoid = customAlphabet('1234567890abcdef', 10)\n\nexport default {\n  props: {\n    value: {\n      type: Object,\n      default: () => ({})\n    }\n  },\n  data() {\n    return {\n      roles: [\n        { text: 'Read Pages', value: 'read:pages', icon: 'mdi-file-eye-outline' },\n        { text: 'Create + Edit Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },\n        { text: 'Rename / Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },\n        { text: 'Delete Pages', value: 'delete:pages', icon: 'mdi-file-remove-outline' },\n        { text: 'View Pages Source', value: 'read:source', icon: 'mdi-code-tags' },\n        { text: 'View Pages History', value: 'read:history', icon: 'mdi-history' },\n        { text: 'Read / Use Assets', value: 'read:assets', icon: 'mdi-image-search-outline' },\n        { text: 'Upload Assets', value: 'write:assets', icon: 'mdi-image-plus' },\n        { text: 'Edit + Delete Assets', value: 'manage:assets', icon: 'mdi-image-size-select-large' },\n        { text: 'Edit Scripts', value: 'write:scripts', icon: 'mdi-language-javascript' },\n        { text: 'Edit Styles', value: 'write:styles', icon: 'mdi-language-css3' },\n        { text: 'Read Comments', value: 'read:comments', icon: 'mdi-comment-search-outline' },\n        { text: 'Create Comments', value: 'write:comments', icon: 'mdi-comment-plus-outline' },\n        { text: 'Edit + Delete Comments', value: 'manage:comments', icon: 'mdi-comment-remove-outline' }\n      ],\n      matches: [\n        { text: 'Path Starts With...', value: 'START', icon: '/...' },\n        { text: 'Path is Exactly...', value: 'EXACT', icon: '=' },\n        { text: 'Path Ends With...', value: 'END', icon: '.../' },\n        { text: 'Path Matches Regex...', value: 'REGEX', icon: '$.*' },\n        { text: 'Tag Matches...', value: 'TAG', icon: 'T' }\n      ]\n    }\n  },\n  computed: {\n    group: {\n      get() { return this.value },\n      set(val) { this.$set('input', val) }\n    },\n    locales() { return siteLangs }\n  },\n  methods: {\n    addRule(group) {\n      this.group.pageRules.push({\n        id: nanoid(),\n        path: '',\n        roles: [],\n        match: 'START',\n        deny: false,\n        locales: []\n      })\n    },\n    removeRule(ruleId) {\n      this.group.pageRules.splice(_.findIndex(this.group.pageRules, ['id', ruleId]), 1)\n    },\n    comingSoon() {\n      this.$store.commit('showNotification', {\n        style: 'indigo',\n        message: `Coming soon...`,\n        icon: 'directions_boat'\n      })\n    },\n    dude (stuff) {\n      console.info(stuff)\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.rules {\n  background-color: mc('blue-grey', '50');\n  border-radius: 4px;\n  padding: 1rem;\n  position: relative;\n\n  @at-root .v-application.theme--dark & {\n    background-color: mc('grey', '800');\n  }\n}\n\n.rule {\n  display: flex;\n  background-color: mc('blue-grey', '100');\n  border-radius: 4px;\n  padding: .5rem;\n  align-items: center;\n\n  &-enter-active, &-leave-active {\n    transition: all .5s ease;\n  }\n  &-enter, &-leave-to {\n    opacity: 0;\n  }\n\n  @at-root .v-application.theme--dark & {\n    background-color: mc('grey', '700');\n  }\n\n  & + .rule {\n    margin-top: .5rem;\n    position: relative;\n\n    &::before {\n      content: '+';\n      position: absolute;\n      width: 2rem;\n      height: 2rem;\n      border-radius: 50%;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      font-weight: 600;\n      color: mc('blue-grey', '700');\n      font-size: 1.25rem;\n      background-color: mc('blue-grey', '50');\n      left: -2rem;\n      top: -1.3rem;\n\n      @at-root .v-application.theme--dark & {\n        background-color: mc('grey', '800');\n        color: mc('grey', '600');\n      }\n    }\n  }\n\n  .input-group + * {\n    margin-left: .5rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-groups-edit-users.vue",
    "content": "<template lang=\"pug\">\n  v-card(flat)\n    v-card-title.pb-4(:class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-5`')\n      v-text-field(\n        outlined\n        flat\n        prepend-inner-icon='mdi-magnify'\n        v-model='search'\n        label='Search Group Users...'\n        hide-details\n        dense\n        style='max-width: 450px;'\n      )\n      v-spacer\n      v-btn(color='primary', depressed, @click='searchUserDialog = true', :disabled='group.id === 2')\n        v-icon(left) mdi-clipboard-account\n        | Assign User\n    v-data-table(\n      :items='group.users',\n      :headers='headers',\n      :search='search'\n      :page.sync='pagination'\n      :items-per-page='15'\n      @page-count='pageCount = $event'\n      must-sort,\n      hide-default-footer\n    )\n      template(v-slot:item.actions='{ item }')\n        v-menu(bottom, right, min-width='200')\n          template(v-slot:activator='{ on }')\n            v-btn(icon, v-on='on', small)\n              v-icon.grey--text.text--darken-1 mdi-dots-horizontal\n          v-list(dense, nav)\n            v-list-item(:to='`/users/` + item.id')\n              v-list-item-action: v-icon(color='primary') mdi-account-outline\n              v-list-item-content\n                v-list-item-title View User Profile\n            template(v-if='item.id !== 2')\n              v-list-item(@click='unassignUser(item.id)')\n                v-list-item-action: v-icon(color='orange') mdi-account-remove-outline\n                v-list-item-content\n                  v-list-item-title Unassign\n      template(slot='no-data')\n        v-alert.ma-3(icon='mdi-alert', outlined) No users to display.\n    .text-center.py-2(v-if='group.users.length > 15')\n      v-pagination(v-model='pagination', :length='pageCount')\n\n    user-search(v-model='searchUserDialog', @select='assignUser')\n</template>\n\n<script>\nimport UserSearch from '../common/user-search.vue'\n\nimport assignUserMutation from 'gql/admin/groups/groups-mutation-assign.gql'\nimport unassignUserMutation from 'gql/admin/groups/groups-mutation-unassign.gql'\n\nexport default {\n  props: {\n    value: {\n      type: Object,\n      default: () => ({})\n    }\n  },\n  components: {\n    UserSearch\n  },\n  data() {\n    return {\n      headers: [\n        { text: 'ID', value: 'id', width: 70 },\n        { text: 'Name', value: 'name' },\n        { text: 'Email', value: 'email' },\n        { text: 'Actions', value: 'actions', sortable: false, width: 50 }\n      ],\n      searchUserDialog: false,\n      pagination: 1,\n      pageCount: 0,\n      search: ''\n    }\n  },\n  computed: {\n    group: {\n      get() { return this.value },\n      set(val) { this.$set('input', val) }\n    },\n    pages () {\n      if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {\n        return 0\n      }\n\n      return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)\n    }\n  },\n  methods: {\n    async assignUser({ id, email, name }) {\n      try {\n        await this.$apollo.mutate({\n          mutation: assignUserMutation,\n          variables: {\n            groupId: this.group.id,\n            userId: id\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-assign')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: `User has been assigned to ${this.group.name}.`,\n          icon: 'assignment_ind'\n        })\n        this.$emit('refresh')\n      } catch (err) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'warning'\n        })\n      }\n    },\n    async unassignUser(id) {\n      try {\n        await this.$apollo.mutate({\n          mutation: unassignUserMutation,\n          variables: {\n            groupId: this.group.id,\n            userId: id\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-unassign')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: `User has been unassigned from ${this.group.name}.`,\n          icon: 'assignment_ind'\n        })\n        this.$emit('refresh')\n      } catch (err) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'warning'\n        })\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-groups-edit.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img(src='/_assets/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2 Edit Group\n            .subtitle-1.grey--text {{group.name}}\n          v-spacer\n          v-btn(color='grey', icon, outlined, to='/groups')\n            v-icon mdi-arrow-left\n          v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem')\n            template(v-slot:activator='{ on }')\n              v-btn.ml-3(color='red', icon, outlined, v-on='on')\n                v-icon(color='red') mdi-trash-can-outline\n            v-card\n              .dialog-header.is-red Delete Group?\n              v-card-text.pa-4 Are you sure you want to delete group #[strong {{ group.name }}]? All users will be unassigned from this group.\n              v-card-actions\n                v-spacer\n                v-btn(text, @click='deleteGroupDialog = false') Cancel\n                v-btn(color='red', dark, @click='deleteGroup') Delete\n          v-btn.ml-3(color='success', large, depressed, @click='updateGroup')\n            v-icon(left) mdi-check\n            span Update Group\n        v-card.mt-3\n          v-tabs.grad-tabs(v-model='tab', :color='$vuetify.theme.dark ? `blue` : `primary`', fixed-tabs, show-arrows, icons-and-text)\n            v-tab(key='settings')\n              span Settings\n              v-icon mdi-cog-box\n            v-tab(key='permissions')\n              span Permissions\n              v-icon mdi-lock-pattern\n            v-tab(key='rules')\n              span Page Rules\n              v-icon mdi-file-lock\n            v-tab(key='users')\n              span Users\n              v-icon mdi-account-group\n\n            v-tab-item(key='settings', :transition='false', :reverse-transition='false')\n              v-card(flat)\n                template(v-if='group.id <= 2')\n                  v-card-text\n                    v-alert.radius-7.mb-0(\n                      color='orange darken-2'\n                      :class='$vuetify.theme.dark ? \"grey darken-4\" : \"orange lighten-5\"'\n                      outlined\n                      :value='true'\n                      icon='mdi-lock-outline'\n                      ) This is a system group and its settings cannot be modified.\n                  v-divider\n                v-card-text\n                  v-text-field(\n                    outlined\n                    v-model='group.name'\n                    label='Group Name'\n                    hide-details\n                    prepend-icon='mdi-account-group'\n                    style='max-width: 600px;'\n                    :disabled='group.id <= 2'\n                  )\n                template(v-if='group.id !== 2')\n                  v-divider\n                  v-card-text\n                    v-text-field(\n                      outlined\n                      v-model='group.redirectOnLogin'\n                      label='Redirect on Login'\n                      persistent-hint\n                      hint='The path / URL where the user will be redirected upon successful login.'\n                      prepend-icon='mdi-arrow-top-left-thick'\n                      append-icon='mdi-folder-search'\n                      @click:append='selectPage'\n                      style='max-width: 850px;'\n                      :counter='255'\n                    )\n\n            v-tab-item(key='permissions', :transition='false', :reverse-transition='false')\n              group-permissions(v-model='group', @refresh='refresh')\n\n            v-tab-item(key='rules', :transition='false', :reverse-transition='false')\n              group-rules(v-model='group', @refresh='refresh')\n\n            v-tab-item(key='users', :transition='false', :reverse-transition='false')\n              group-users(v-model='group', @refresh='refresh')\n\n          v-card-chin\n            v-spacer\n            .caption.grey--text.pr-2 Group ID #[strong {{group.id}}]\n\n    page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nimport GroupPermissions from './admin-groups-edit-permissions.vue'\nimport GroupRules from './admin-groups-edit-rules.vue'\nimport GroupUsers from './admin-groups-edit-users.vue'\n\n/* global siteConfig */\n\nexport default {\n  components: {\n    GroupPermissions,\n    GroupRules,\n    GroupUsers\n  },\n  data() {\n    return {\n      group: {\n        id: 0,\n        name: '',\n        isSystem: false,\n        permissions: [],\n        pageRules: [],\n        users: [],\n        redirectOnLogin: '/'\n      },\n      deleteGroupDialog: false,\n      tab: null,\n      selectPageModal: false,\n      currentLang: siteConfig.lang\n    }\n  },\n  methods: {\n    selectPage () {\n      this.selectPageModal = true\n    },\n    selectPageHandle ({ path, locale }) {\n      this.group.redirectOnLogin = `/${locale}/${path}`\n    },\n    async updateGroup() {\n      try {\n        await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $id: Int!\n              $name: String!\n              $redirectOnLogin: String!\n              $permissions: [String]!\n              $pageRules: [PageRuleInput]!\n            ) {\n              groups {\n                update(\n                  id: $id\n                  name: $name\n                  redirectOnLogin: $redirectOnLogin\n                  permissions: $permissions\n                  pageRules: $pageRules\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.group.id,\n            name: this.group.name,\n            redirectOnLogin: this.group.redirectOnLogin,\n            permissions: this.group.permissions,\n            pageRules: this.group.pageRules\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-update')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: `Group changes have been saved.`,\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    },\n    async deleteGroup() {\n      this.deleteGroupDialog = false\n      try {\n        await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($id: Int!) {\n              groups {\n                delete(id: $id) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.group.id\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-delete')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: `Group ${this.group.name} has been deleted.`,\n          icon: 'delete'\n        })\n        this.$router.replace('/groups')\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    },\n    async refresh() {\n      return this.$apollo.queries.group.refetch()\n    }\n  },\n  apollo: {\n    group: {\n      query: gql`\n        query ($id: Int!) {\n          groups {\n            single(id: $id) {\n              id\n              name\n              redirectOnLogin\n              isSystem\n              permissions\n              pageRules {\n                id\n                path\n                roles\n                match\n                deny\n                locales\n              }\n              users {\n                id\n                name\n                email\n              }\n              createdAt\n              updatedAt\n            }\n          }\n        }\n      `,\n      variables() {\n        return {\n          id: _.toSafeInteger(this.$route.params.id)\n        }\n      },\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.groups.single),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-groups.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-people.svg', alt='Groups', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2.animated.fadeInLeft Groups\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s Manage groups and their permissions\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/groups', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.animated.fadeInDown.wait-p2s.mx-3(color='grey', outlined, @click='refresh', icon)\n            v-icon mdi-refresh\n          v-dialog(v-model='newGroupDialog', max-width='500')\n            template(v-slot:activator='{ on }')\n              v-btn.animated.fadeInDown(color='primary', depressed, v-on='on', large)\n                v-icon(left) mdi-plus\n                span New Group\n            v-card\n              .dialog-header.is-short New Group\n              v-card-text.pt-5\n                v-text-field.md2(\n                  outlined\n                  prepend-icon='mdi-account-group'\n                  v-model='newGroupName'\n                  label='Group Name'\n                  counter='255'\n                  @keyup.enter='createGroup'\n                  @keyup.esc='newGroupDialog = false'\n                  ref='groupNameIpt'\n                  )\n              v-card-chin\n                v-spacer\n                v-btn(text, @click='newGroupDialog = false') Cancel\n                v-btn(color='primary', @click='createGroup') Create\n        v-card.mt-3.animated.fadeInUp\n          v-data-table(\n            :items='groups'\n            :headers='headers'\n            :search='search'\n            :page.sync='pagination'\n            :items-per-page='15'\n            :loading='loading'\n            @page-count='pageCount = $event'\n            must-sort,\n            hide-default-footer\n          )\n            template(slot='item', slot-scope='props')\n              tr.is-clickable(:active='props.selected', @click='$router.push(\"/groups/\" + props.item.id)')\n                td {{ props.item.id }}\n                td: strong {{ props.item.name }}\n                td {{ props.item.userCount }}\n                td {{ props.item.createdAt | moment('calendar') }}\n                td {{ props.item.updatedAt | moment('calendar') }}\n                td\n                  v-tooltip(left, v-if='props.item.isSystem')\n                    template(v-slot:activator='{ on }')\n                      v-icon(v-on='on') mdi-lock-outline\n                    span System Group\n            template(slot='no-data')\n              v-alert.ma-3(icon='mdi-alert', :value='true', outline) No groups to display.\n          .text-xs-center.py-2(v-if='pageCount > 1')\n            v-pagination(v-model='pagination', :length='pageCount')\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport groupsQuery from 'gql/admin/groups/groups-query-list.gql'\nimport createGroupMutation from 'gql/admin/groups/groups-mutation-create.gql'\n\nexport default {\n  data() {\n    return {\n      newGroupDialog: false,\n      newGroupName: '',\n      selectedGroup: {},\n      pagination: 1,\n      pageCount: 0,\n      groups: [],\n      headers: [\n        { text: 'ID', value: 'id', width: 80, sortable: true },\n        { text: 'Name', value: 'name' },\n        { text: 'Users', value: 'userCount', width: 200 },\n        { text: 'Created', value: 'createdAt', width: 250 },\n        { text: 'Last Updated', value: 'updatedAt', width: 250 },\n        { text: '', value: 'isSystem', width: 20, sortable: false }\n      ],\n      search: '',\n      loading: false\n    }\n  },\n  watch: {\n    newGroupDialog(newValue, oldValue) {\n      if (newValue) {\n        this.$nextTick(() => {\n          this.$refs.groupNameIpt.focus()\n        })\n      }\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.groups.refetch()\n      this.$store.commit('showNotification', {\n        message: 'Groups have been refreshed.',\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async createGroup() {\n      if (_.trim(this.newGroupName).length < 1) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: 'Enter a group name.',\n          icon: 'warning'\n        })\n        return\n      }\n      this.newGroupDialog = false\n      try {\n        await this.$apollo.mutate({\n          mutation: createGroupMutation,\n          variables: {\n            name: this.newGroupName\n          },\n          update (store, resp) {\n            const data = _.get(resp, 'data.groups.create', { responseResult: {} })\n            if (data.responseResult.succeeded === true) {\n              const apolloData = store.readQuery({ query: groupsQuery })\n              data.group.userCount = 0\n              apolloData.groups.list.push(data.group)\n              store.writeQuery({ query: groupsQuery, data: apolloData })\n            } else {\n              throw new Error(data.responseResult.message)\n            }\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-create')\n          }\n        })\n        this.newGroupName = ''\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: `Group has been created successfully.`,\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    }\n  },\n  apollo: {\n    groups: {\n      query: groupsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.loading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-locale.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-globe-earth.svg', alt='Locale', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:locale.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:locale.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/locales', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.animated.fadeInDown.ml-3(color='success', depressed, @click='save', large, :loading='loading')\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(xl6 lg5 xs12)\n              v-card.wiki-form.animated.fadeInUp\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.settings') }}\n                v-card-text\n                  v-select(\n                    outlined\n                    :items='installedLocales'\n                    prepend-icon='mdi-web'\n                    v-model='selectedLocale'\n                    item-value='code'\n                    item-text='nativeName'\n                    :label='namespacing ? $t(\"admin:locale.base.labelWithNS\") : $t(\"admin:locale.base.label\")'\n                    persistent-hint\n                    :hint='$t(\"admin:locale.base.hint\")'\n                  )\n                    template(slot='item', slot-scope='data')\n                      template(v-if='typeof data.item !== \"object\"')\n                        v-list-item-content(v-text='data.item')\n                      template(v-else)\n                        v-list-item-avatar\n                          v-avatar.blue.white--text(tile, size='40', v-html='data.item.code.toUpperCase()')\n                        v-list-item-content\n                          v-list-item-title(v-html='data.item.name')\n                          v-list-item-subtitle(v-html='data.item.nativeName')\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    v-model='autoUpdate'\n                    :label='$t(\"admin:locale.autoUpdate.label\")'\n                    color='primary'\n                    persistent-hint\n                    :hint='namespacing ? $t(\"admin:locale.autoUpdate.hintWithNS\") : $t(\"admin:locale.autoUpdate.hint\")'\n                  )\n\n              v-card.wiki-form.mt-3.animated.fadeInUp.wait-p2s\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.namespacing') }}\n                v-card-text\n                  v-switch(\n                    inset\n                    v-model='namespacing'\n                    :label='$t(\"admin:locale.namespaces.label\")'\n                    color='primary'\n                    persistent-hint\n                    :hint='$t(\"admin:locale.namespaces.hint\")'\n                    )\n                  v-alert.mt-3(\n                    outlined\n                    color='orange'\n                    :value='true'\n                    icon='mdi-alert'\n                    )\n                    span {{ $t('admin:locale.namespacingPrefixWarning.title', { langCode: selectedLocale }) }}\n                    .caption.grey--text {{ $t('admin:locale.namespacingPrefixWarning.subtitle') }}\n                  v-divider.mt-3.mb-4\n                  v-select(\n                    outlined\n                    :disabled='!namespacing'\n                    :items='installedLocales'\n                    prepend-icon='mdi-web'\n                    multiple\n                    chips\n                    deletable-chips\n                    v-model='namespaces'\n                    item-value='code'\n                    item-text='name'\n                    :label='$t(\"admin:locale.activeNamespaces.label\")'\n                    persistent-hint\n                    small-chips\n                    :hint='$t(\"admin:locale.activeNamespaces.hint\")'\n                    )\n                    template(slot='item', slot-scope='data')\n                      template(v-if='typeof data.item !== \"object\"')\n                        v-list-item-content(v-text='data.item')\n                      template(v-else)\n                        v-list-item-avatar\n                          v-avatar.blue.white--text(tile, size='40', v-html='data.item.code.toUpperCase()')\n                        v-list-item-content\n                          v-list-item-title(v-html='data.item.name')\n                          v-list-item-subtitle(v-html='data.item.nativeName')\n                        v-list-item-action\n                          v-checkbox(:input-value='data.attrs.inputValue', color='primary', value)\n            v-flex(xl6 lg7 xs12)\n              v-card.animated.fadeInUp.wait-p4s\n                v-toolbar(color='teal', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.downloadTitle') }}\n                v-data-table(\n                  :headers='headers',\n                  :items='locales',\n                  hide-default-footer,\n                  item-key='code',\n                  :items-per-page='1000'\n                  )\n                  template(v-slot:item.code='{ item }')\n                    v-chip.white--text(label, color='teal', small) {{item.code}}\n                  template(v-slot:item.name='{ item }')\n                    strong {{item.name}}\n                  template(v-slot:item.isRTL='{ item }')\n                    v-icon(v-if='item.isRTL') mdi-check\n                  template(v-slot:item.availability='{ item }')\n                    .d-flex.align-center.pl-4\n                      v-progress-circular(:value='item.availability', width='2', size='20', :color='item.availability <= 33 ? `red` : (item.availability <= 66) ? `orange` : `green`')\n                      .caption.mx-2(:class='item.availability <= 33 ? `red--text` : (item.availability <= 66) ? `orange--text` : `green--text`') {{item.availability}}%\n                  template(v-slot:item.isInstalled='{ item }')\n                    v-progress-circular(v-if='item.isDownloading', indeterminate, color='blue', size='20', :width='2')\n                    v-btn(v-else-if='item.isInstalled && item.installDate < item.updatedAt', icon, small, @click='download(item)')\n                      v-icon.blue--text mdi-cached\n                    v-btn(v-else-if='item.isInstalled', icon, small, @click='download(item)')\n                      v-icon.green--text mdi-check-bold\n                    v-btn(v-else, icon, small, @click='download(item)')\n                      v-icon.grey--text mdi-cloud-download\n              v-card.wiki-form.mt-3.animated.fadeInUp.wait-p5s\n                v-toolbar(color='teal', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.sideload') }}\n                  v-spacer\n                  v-chip(label, color='white', small).teal--text coming soon\n                v-card-text\n                  div {{ $t('admin:locale.sideloadHelp') }}\n                  v-btn.ml-0.mt-3(color='teal', disabled) {{ $t('common:actions.browse') }}\n</template>\n\n<script>\nimport _ from 'lodash'\n\n/* global WIKI */\n\nimport localesQuery from 'gql/admin/locale/locale-query-list.gql'\nimport localesDownloadMutation from 'gql/admin/locale/locale-mutation-download.gql'\nimport localesSaveMutation from 'gql/admin/locale/locale-mutation-save.gql'\n\nexport default {\n  data() {\n    return {\n      loading: false,\n      locales: [],\n      selectedLocale: 'en',\n      autoUpdate: false,\n      namespacing: false,\n      namespaces: []\n    }\n  },\n  computed: {\n    installedLocales() {\n      return _.filter(this.locales, ['isInstalled', true])\n    },\n    headers() {\n      return [\n        {\n          text: this.$t('admin:locale.code'),\n          align: 'left',\n          value: 'code',\n          width: 90\n        },\n        {\n          text: this.$t('admin:locale.name'),\n          align: 'left',\n          value: 'name'\n        },\n        {\n          text: this.$t('admin:locale.nativeName'),\n          align: 'left',\n          value: 'nativeName'\n        },\n        {\n          text: this.$t('admin:locale.rtl'),\n          align: 'center',\n          value: 'isRTL',\n          sortable: false,\n          width: 10\n        },\n        {\n          text: this.$t('admin:locale.availability'),\n          align: 'center',\n          value: 'availability',\n          sortable: false,\n          width: 120\n        },\n        {\n          text: this.$t('admin:locale.download'),\n          align: 'center',\n          value: 'isInstalled',\n          sortable: false,\n          width: 100\n        }\n      ]\n    }\n  },\n  methods: {\n    async download(lc) {\n      lc.isDownloading = true\n      const respRaw = await this.$apollo.mutate({\n        mutation: localesDownloadMutation,\n        variables: {\n          locale: lc.code\n        }\n      })\n      const resp = _.get(respRaw, 'data.localization.downloadLocale.responseResult', {})\n      if (resp.succeeded) {\n        lc.isDownloading = false\n        lc.isInstalled = true\n        lc.updatedAt = new Date().toISOString()\n        lc.installDate = lc.updatedAt\n        this.$store.commit('showNotification', {\n          message: `Locale ${lc.name} has been installed successfully.`,\n          style: 'success',\n          icon: 'get_app'\n        })\n      } else {\n        this.$store.commit('showNotification', {\n          message: `Error: ${resp.message}`,\n          style: 'error',\n          icon: 'warning'\n        })\n      }\n      this.isDownloading = false\n    },\n    async save() {\n      this.loading = true\n      const respRaw = await this.$apollo.mutate({\n        mutation: localesSaveMutation,\n        variables: {\n          locale: this.selectedLocale,\n          autoUpdate: this.autoUpdate,\n          namespacing: this.namespacing,\n          namespaces: this.namespaces\n        }\n      })\n      const resp = _.get(respRaw, 'data.localization.updateLocale.responseResult', {})\n      if (resp.succeeded) {\n        // Change UI language\n        WIKI.$i18n.i18next.changeLanguage(this.selectedLocale)\n        WIKI.$moment.locale(this.selectedLocale)\n\n        // Check for RTL\n        const curLocale = _.find(this.locales, ['code', this.selectedLocale])\n        this.$vuetify.rtl = curLocale && curLocale.isRTL\n\n        this.$store.commit('showNotification', {\n          message: 'Locale settings updated successfully.',\n          style: 'success',\n          icon: 'check'\n        })\n\n        _.delay(() => {\n          window.location.reload(true)\n        }, 1000)\n      } else {\n        this.$store.commit('showNotification', {\n          message: `Error: ${resp.message}`,\n          style: 'error',\n          icon: 'warning'\n        })\n      }\n      this.loading = false\n    }\n  },\n  apollo: {\n    locales: {\n      query: localesQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.localization.locales.map(lc => ({ ...lc, isDownloading: false })),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-locale-refresh')\n      }\n    },\n    selectedLocale: {\n      query: localesQuery,\n      update: (data) => data.localization.config.locale\n    },\n    autoUpdate: {\n      query: localesQuery,\n      update: (data) => data.localization.config.autoUpdate\n    },\n    namespacing: {\n      query: localesQuery,\n      update: (data) => data.localization.config.namespacing\n    },\n    namespaces: {\n      query: localesQuery,\n      update: (data) => data.localization.config.namespaces\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-logging-console.vue",
    "content": "<template lang='pug'>\n  v-dialog(v-model='isShown', width='90vw', max-width='1200')\n    .dialog-header\n      span Live Console\n      v-spacer\n      .caption.blue--text.text--lighten-3.mr-3 Streaming...\n      v-progress-circular(\n        indeterminate\n        color='blue lighten-3'\n        :size='20'\n        :width='2'\n        )\n    .consoleTerm(ref='consoleContainer')\n    v-toolbar(flat, color='grey darken-3', dark)\n      v-spacer\n      v-btn(outline, @click='clear')\n        v-icon(left) cancel_presentation\n        span Clear\n      v-btn(outline, @click='close')\n        v-icon(left) close\n        span Close\n</template>\n\n<script>\nimport _ from 'lodash'\n// import { Terminal } from 'xterm'\n// import * as fit from 'xterm/lib/addons/fit/fit'\n\nimport livetrailSubscription from 'gql/admin/logging/logging-subscription-livetrail.gql'\n\n// Terminal.applyAddon(fit)\n\nexport default {\n  term: null,\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    }\n  },\n  watch: {\n    value(newValue, oldValue) {\n      if (newValue) {\n        _.delay(() => {\n          // this.term = new Terminal()\n          this.term.open(this.$refs.consoleContainer)\n          this.term.writeln('Connecting to \\x1B[1;3;31mconsole output\\x1B[0m...')\n\n          this.attach()\n        }, 100)\n      } else {\n        this.term.dispose()\n        this.term = null\n      }\n    }\n  },\n  mounted() {\n\n  },\n  methods: {\n    clear() {\n      this.term.clear()\n    },\n    close() {\n      this.isShown = false\n    },\n    attach() {\n      const self = this\n      const observer = this.$apollo.subscribe({\n        query: livetrailSubscription\n      })\n      observer.subscribe({\n        next(data) {\n          const item = _.get(data, `data.loggingLiveTrail`, {})\n          console.info(item)\n          self.term.writeln(`${item.level}: ${item.output}`)\n        },\n        error(error) {\n          self.$store.commit('showNotification', {\n            style: 'red',\n            message: error.message,\n            icon: 'warning'\n          })\n        }\n      })\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.consoleTerm {\n  background-color: #000;\n  padding: 16px;\n  width: 100%;\n  height: 415px;\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-logging.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img(src='/_assets/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text Logging\n            .subtitle-1.grey--text Configure the system logger(s) #[v-chip(label, color='primary', small).white--text coming soon]\n          v-spacer\n          v-btn(outline, color='grey', @click='refresh', large)\n            v-icon refresh\n          v-btn(color='black', disabled, depressed, @click='toggleConsole', large)\n            v-icon check\n            span Live Trail\n          v-btn(color='success', @click='save', depressed, large)\n            v-icon(left) check\n            span {{$t('common:actions.apply')}}\n\n        v-card.mt-3\n          v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark)\n            v-tab(key='settings'): v-icon settings\n            v-tab(v-for='logger in activeLoggers', :key='logger.key') {{ logger.title }}\n\n            v-tab-item(key='settings', :transition='false', :reverse-transition='false')\n              v-card.pa-3(flat, tile)\n                .body-2.grey--text.text--darken-1 Select which logging service to enable:\n                .caption.grey--text.pb-2 Some loggers require additional configuration in their dedicated tab (when selected).\n                v-form\n                  v-checkbox.my-0(\n                    v-for='(logger, n) in loggers'\n                    v-model='logger.isEnabled'\n                    :key='logger.key'\n                    :label='logger.title'\n                    color='primary'\n                    hide-details\n                    disabled\n                  )\n\n            v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')\n              v-card.wiki-form.pa-3(flat, tile)\n                v-form\n                  .loggerlogo\n                    img(:src='logger.logo', :alt='logger.title')\n                  v-subheader.pl-0 {{logger.title}}\n                  .caption {{logger.description}}\n                  .caption: a(:href='logger.website') {{logger.website}}\n                  v-divider.mt-3\n                  v-subheader.pl-0 Logger Configuration\n                  .body-1.ml-3(v-if='!logger.config || logger.config.length < 1') This logger has no configuration options you can modify.\n                  template(v-else, v-for='cfg in logger.config')\n                    v-select(\n                      v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                      outline\n                      background-color='grey lighten-2'\n                      :items='cfg.value.enum'\n                      :key='cfg.key'\n                      :label='cfg.value.title'\n                      v-model='cfg.value.value'\n                      prepend-icon='settings_applications'\n                      :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                      persistent-hint\n                      :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                    )\n                    v-switch(\n                      v-else-if='cfg.value.type === \"boolean\"'\n                      :key='cfg.key'\n                      :label='cfg.value.title'\n                      v-model='cfg.value.value'\n                      color='primary'\n                      prepend-icon='settings_applications'\n                      :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                      persistent-hint\n                      )\n                    v-text-field(\n                      v-else\n                      outline\n                      background-color='grey lighten-2'\n                      :key='cfg.key'\n                      :label='cfg.value.title'\n                      v-model='cfg.value.value'\n                      prepend-icon='settings_applications'\n                      :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                      persistent-hint\n                      :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                      )\n                  v-divider.mt-3\n                  v-subheader.pl-0 Log Level\n                  .body-1.ml-3 Select the minimum error level that will be reported to this logger.\n                  v-layout(row)\n                    v-flex(xs12, md6, lg4)\n                      .pt-3\n                        v-select(\n                          single-line\n                          outline\n                          background-color='grey lighten-2'\n                          :items='levels'\n                          label='Level'\n                          v-model='logger.level'\n                          prepend-icon='graphic_eq'\n                          hint='Default: warn'\n                          persistent-hint\n                        )\n\n    logging-console(v-model='showConsole')\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport LoggingConsole from './admin-logging-console.vue'\n\nimport loggersQuery from 'gql/admin/logging/logging-query-loggers.gql'\nimport loggersSaveMutation from 'gql/admin/logging/logging-mutation-save-loggers.gql'\n\nexport default {\n  components: {\n    LoggingConsole\n  },\n  data() {\n    return {\n      showConsole: false,\n      loggers: [],\n      levels: ['error', 'warn', 'info', 'debug', 'verbose']\n    }\n  },\n  computed: {\n    activeLoggers() {\n      return _.filter(this.loggers, 'isEnabled')\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.loggers.refetch()\n      this.$store.commit('showNotification', {\n        message: 'List of loggers has been refreshed.',\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-logging-saveloggers')\n      await this.$apollo.mutate({\n        mutation: loggersSaveMutation,\n        variables: {\n          loggers: this.loggers.map(tgt => _.pick(tgt, [\n            'isEnabled',\n            'key',\n            'config',\n            'level'\n          ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))\n        }\n      })\n      this.$store.commit('showNotification', {\n        message: 'Logging configuration saved successfully.',\n        style: 'success',\n        icon: 'check'\n      })\n      this.$store.commit(`loadingStop`, 'admin-logging-saveloggers')\n    },\n    toggleConsole() {\n      this.showConsole = !this.showConsole\n    }\n  },\n  apollo: {\n    loggers: {\n      query: loggersQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.logging.loggers).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-logging-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss' scoped>\n\n.loggerlogo {\n  width: 250px;\n  height: 85px;\n  float:right;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n\n  img {\n    max-width: 100%;\n    max-height: 50px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-mail.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-new-post.svg', alt='Mail', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:mail.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:mail.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(lg6 xs12)\n              v-form\n                v-card.animated.fadeInUp\n                  v-toolbar(color='primary', dark, dense, flat)\n                    v-toolbar-title.subtitle-1 {{ $t('admin:mail.configuration') }}\n                  .overline.pa-4.grey--text {{ $t('admin:mail.sender') }}\n                  .px-4\n                    v-text-field(\n                      outlined\n                      v-model='config.senderName'\n                      :label='$t(`admin:mail.senderName`)'\n                      required\n                      :counter='255'\n                      prepend-icon='mdi-mailbox'\n                      )\n                    v-text-field(\n                      outlined\n                      v-model='config.senderEmail'\n                      :label='$t(`admin:mail.senderEmail`)'\n                      required\n                      :counter='255'\n                      prepend-icon='mdi-mailbox'\n                      )\n                  v-divider\n                  .overline.pa-4.grey--text {{ $t('admin:mail.smtp') }}\n                  .px-4\n                    v-text-field(\n                      outlined\n                      v-model='config.host'\n                      :label='$t(`admin:mail.smtpHost`)'\n                      required\n                      :counter='255'\n                      prepend-icon='mdi-memory'\n                      )\n                    v-text-field(\n                      outlined\n                      v-model='config.port'\n                      :label='$t(`admin:mail.smtpPort`)'\n                      required\n                      prepend-icon='mdi-serial-port'\n                      persistent-hint\n                      :hint='$t(`admin:mail.smtpPortHint`)'\n                      style='max-width: 300px;'\n                      )\n                    v-text-field(\n                      outlined\n                      v-model='config.name'\n                      :label='$t(`admin:mail.smtpName`)'\n                      required\n                      :counter='255'\n                      prepend-icon='mdi-server'\n                      persistent-hint\n                      :hint='$t(`admin:mail.smtpNameHint`)'\n                      )\n                    v-switch(\n                      v-model='config.secure'\n                      :label='$t(`admin:mail.smtpTLS`)'\n                      color='primary'\n                      persistent-hint\n                      :hint='$t(`admin:mail.smtpTLSHint`)'\n                      prepend-icon='mdi-security-network'\n                      inset\n                      )\n                    v-switch(\n                      v-model='config.verifySSL'\n                      :label='$t(`admin:mail.smtpVerifySSL`)'\n                      color='primary'\n                      persistent-hint\n                      :hint='$t(`admin:mail.smtpVerifySSLHint`)'\n                      prepend-icon='mdi-security-network'\n                      inset\n                      )\n                    v-text-field.mt-8(\n                      outlined\n                      v-model='config.user'\n                      :label='$t(`admin:mail.smtpUser`)'\n                      required\n                      :counter='255'\n                      prepend-icon='mdi-shield-account-outline'\n                      )\n                    v-text-field(\n                      outlined\n                      v-model='config.pass'\n                      :label='$t(`admin:mail.smtpPwd`)'\n                      required\n                      prepend-icon='mdi-form-textbox-password'\n                      type='password'\n                      )\n\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp.wait-p2s\n                v-form\n                  v-toolbar(color='primary', dark, dense, flat)\n                    v-toolbar-title.subtitle-1 {{ $t('admin:mail.dkim') }}\n                  v-card-info\n                    span {{ $t('admin:mail.dkimHint') }}\n                  .pa-4\n                    v-switch(\n                      v-model='config.useDKIM'\n                      :label='$t(`admin:mail.dkimUse`)'\n                      color='primary'\n                      prepend-icon='mdi-key'\n                      inset\n                      )\n                    v-text-field(\n                      outlined\n                      v-model='config.dkimDomainName'\n                      :label='$t(`admin:mail.dkimDomainName`)'\n                      :counter='255'\n                      prepend-icon='mdi-key'\n                      :disabled='!config.useDKIM'\n                      )\n                    v-text-field(\n                      outlined\n                      v-model='config.dkimKeySelector'\n                      :label='$t(`admin:mail.dkimKeySelector`)'\n                      :counter='255'\n                      prepend-icon='mdi-key'\n                      :disabled='!config.useDKIM'\n                      )\n                    v-textarea(\n                      outlined\n                      v-model='config.dkimPrivateKey'\n                      :label='$t(`admin:mail.dkimPrivateKey`)'\n                      prepend-icon='mdi-key'\n                      persistent-hint\n                      :hint='$t(`admin:mail.dkimPrivateKeyHint`)'\n                      :disabled='!config.useDKIM'\n                      )\n\n              v-card.mt-3.animated.fadeInUp.wait-p3s\n                v-form\n                  v-toolbar(color='teal', dark, dense, flat)\n                    v-toolbar-title.subtitle-1 {{ $t('admin:mail.test') }}\n                  .pa-4\n                    .body-2.grey--text.text--darken-2 {{ $t('admin:mail.testHint') }}\n                    v-text-field.mt-3(\n                      outlined\n                      v-model='testEmail'\n                      :label='$t(`admin:mail.testRecipient`)'\n                      :counter='255'\n                      prepend-icon='mdi-email-outline'\n                      :disabled='testLoading'\n                      )\n                  v-card-chin\n                    v-spacer\n                    v-btn.px-4(color='teal', dark, @click='sendTest', :loading='testLoading')\n                      v-icon(left) mdi-send\n                      span {{ $t('admin:mail.testSend') }}\n\n</template>\n\n<script>\nimport _ from 'lodash'\nimport mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'\nimport mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'\nimport mailTestMutation from 'gql/admin/mail/mail-mutation-sendtest.gql'\n\nexport default {\n  data() {\n    return {\n      config: {\n        senderName: '',\n        senderEmail: '',\n        host: '',\n        port: 0,\n        name: '',\n        secure: false,\n        verifySSL: false,\n        user: '',\n        pass: '',\n        useDKIM: false,\n        dkimDomainName: '',\n        dkimKeySelector: '',\n        dkimPrivateKey: ''\n      },\n      testEmail: '',\n      testLoading: false\n    }\n  },\n  methods: {\n    async save () {\n      try {\n        await this.$apollo.mutate({\n          mutation: mailUpdateConfigMutation,\n          variables: {\n            senderName: this.config.senderName || '',\n            senderEmail: this.config.senderEmail || '',\n            host: this.config.host || '',\n            port: _.toSafeInteger(this.config.port) || 0,\n            name: this.config.name || '',\n            secure: this.config.secure || false,\n            verifySSL: this.config.verifySSL || false,\n            user: this.config.user || '',\n            pass: this.config.pass || '',\n            useDKIM: this.config.useDKIM || false,\n            dkimDomainName: this.config.dkimDomainName || '',\n            dkimKeySelector: this.config.dkimKeySelector || '',\n            dkimPrivateKey: this.config.dkimPrivateKey || ''\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:mail.saveSuccess'),\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    },\n    async sendTest () {\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: mailTestMutation,\n          variables: {\n            recipientEmail: this.testEmail\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-test')\n          }\n        })\n        if (!_.get(resp, 'data.mail.sendTest.responseResult.succeeded', false)) {\n          throw new Error(_.get(resp, 'data.mail.sendTest.responseResult.message', 'An unexpected error occurred.'))\n        }\n\n        this.testEmail = ''\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:mail.sendTestSuccess'),\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    }\n  },\n  apollo: {\n    config: {\n      query: mailConfigQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.mail.config),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-navigation.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('navigation.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('navigation.subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/navigation', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.mx-3.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n        v-container.pa-0.mt-3(fluid, grid-list-lg)\n          v-row(dense)\n            v-col(cols='3')\n              v-card.animated.fadeInUp\n                v-toolbar(color='teal', dark, dense, flat, height='56')\n                  v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}}\n                v-list(nav, two-line)\n                  v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')\n                    v-list-item(value='TREE')\n                      v-list-item-avatar\n                        img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree')\n                      v-list-item-content\n                        v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}}\n                        v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}}\n                      v-list-item-avatar\n                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `TREE` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle\n                        v-icon(v-else, :color='config.mode === `TREE` ? `teal` : `grey lighten-3`') mdi-check-circle\n                    v-list-item(value='STATIC')\n                      v-list-item-avatar\n                        img(src='/_assets/svg/icon-features-list.svg', alt='Static Navigation')\n                      v-list-item-content\n                        v-list-item-title {{$t('admin:navigation.modeStatic.title')}}\n                        v-list-item-subtitle {{$t('admin:navigation.modeStatic.description')}}\n                      v-list-item-avatar\n                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `STATIC` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle\n                        v-icon(v-else, :color='config.mode === `STATIC` ? `teal` : `grey lighten-3`') mdi-check-circle\n                    v-list-item(value='MIXED')\n                      v-list-item-avatar\n                        img(src='/_assets/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')\n                      v-list-item-content\n                        v-list-item-title {{$t('admin:navigation.modeCustom.title')}}\n                        v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}}\n                      v-list-item-avatar\n                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `MIXED` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle\n                        v-icon(v-else, :color='config.mode === `MIXED` ? `teal` : `grey lighten-3`') mdi-check-circle\n                    v-list-item(value='NONE')\n                      v-list-item-avatar\n                        img(src='/_assets/svg/icon-cancel-dotted.svg', alt='None')\n                      v-list-item-content\n                        v-list-item-title {{$t('admin:navigation.modeNone.title')}}\n                        v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}}\n                      v-list-item-avatar\n                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle\n                        v-icon(v-else, :color='config.mode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle\n            v-col(cols='9', v-if='config.mode === `MIXED` || config.mode === `STATIC`')\n              v-card.animated.fadeInUp.wait-p2s\n                v-row(no-gutters, align='stretch')\n                  v-col(style='flex: 0 0 350px;')\n                    v-card.grey(flat, style='height: 100%; border-radius: 4px 0 0 4px;', :class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-3`')\n                      .teal.lighten-1.pa-2.d-flex(style='margin-bottom: 1px; height:56px;')\n                        v-select(\n                          :disabled='locales.length < 2'\n                          label='Locale'\n                          hide-details\n                          solo\n                          flat\n                          background-color='teal darken-2'\n                          dark\n                          dense\n                          v-model='currentLang'\n                          :items='locales'\n                          item-text='nativeName'\n                          item-value='code'\n                        )\n                        v-tooltip(top)\n                          template(v-slot:activator='{ on }')\n                            v-btn.ml-2(icon, tile, color='white', v-on='on', @click='copyFromLocaleDialogIsShown = true')\n                              v-icon mdi-arrange-send-backward\n                          span {{$t('admin:navigation.copyFromLocale')}}\n                      v-list.py-2(dense, nav, dark, class='blue darken-2', style='border-radius: 0;')\n                        v-list-item(v-if='currentTree.length < 1')\n                          v-list-item-avatar(size='24'): v-icon(color='blue lighten-3') mdi-alert\n                          v-list-item-content\n                            em.caption.blue--text.text--lighten-4 {{$t('navigation.emptyList')}}\n                        draggable(v-model='currentTree')\n                          template(v-for='navItem in currentTree')\n                            v-list-item(\n                              v-if='navItem.kind === \"link\"'\n                              :key='navItem.id'\n                              :class='(navItem === current) ? \"blue\" : \"\"'\n                              @click='selectItem(navItem)'\n                              )\n                              v-list-item-avatar(size='24', tile)\n                                v-icon(v-if='navItem.icon.match(/fa[a-z] fa-/)', size='19') {{ navItem.icon }}\n                                v-icon(v-else) {{ navItem.icon }}\n                              v-list-item-title {{navItem.label}}\n                            .py-2.clickable(\n                              v-else-if='navItem.kind === \"divider\"'\n                              :key='navItem.id'\n                              :class='(navItem === current) ? \"blue\" : \"\"'\n                              @click='selectItem(navItem)'\n                              )\n                              v-divider\n                            v-subheader.pl-4.clickable(\n                              v-else-if='navItem.kind === \"header\"'\n                              :key='navItem.id'\n                              :class='(navItem === current) ? \"blue\" : \"\"'\n                              @click='selectItem(navItem)'\n                              ) {{navItem.label}}\n                      v-card-chin\n                        v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;')\n                          template(v-slot:activator='{ on }')\n                            v-btn(v-on='on', color='primary', depressed, block)\n                              v-icon(left) mdi-plus\n                              span {{$t('common:actions.add')}}\n                          v-list\n                            v-list-item(@click='addItem(\"link\")')\n                              v-list-item-avatar(size='24'): v-icon mdi-link\n                              v-list-item-title {{$t('navigation.link')}}\n                            v-list-item(@click='addItem(\"header\")')\n                              v-list-item-avatar(size='24'): v-icon mdi-format-title\n                              v-list-item-title {{$t('navigation.header')}}\n                            v-list-item(@click='addItem(\"divider\")')\n                              v-list-item-avatar(size='24'): v-icon mdi-minus\n                              v-list-item-title {{$t('navigation.divider')}}\n                  v-col\n                    v-card(flat, style='border-radius: 0 4px 4px 0;')\n                      template(v-if='current.kind === \"link\"')\n                        v-toolbar(height='56', color='teal lighten-1', flat, dark)\n                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}}\n                          v-spacer\n                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')\n                            v-icon(left) mdi-delete\n                            span {{$t('navigation.delete', { kind: $t('navigation.link') })}}\n                        v-card-text\n                          v-text-field(\n                            outlined\n                            :label='$t(\"navigation.label\")'\n                            prepend-icon='mdi-format-title'\n                            v-model='current.label'\n                            counter='255'\n                          )\n                          v-text-field(\n                            outlined\n                            :label='$t(\"navigation.icon\")'\n                            prepend-icon='mdi-dice-5'\n                            v-model='current.icon'\n                            hide-details\n                          )\n                          .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section.\n                          .caption.pt-3.pl-5: strong Material Design Icons\n                          .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home]\n                          .caption.pt-3.pl-5: strong Font Awesome 5\n                          .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]).\n                          .caption.pt-3.pl-5: strong Font Awesome 4\n                          .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home]\n                        v-divider\n                        v-card-text\n                          v-select(\n                            outlined\n                            :label='$t(\"navigation.targetType\")'\n                            prepend-icon='mdi-near-me'\n                            :items='navTypes'\n                            v-model='current.targetType'\n                            hide-details\n                          )\n                          v-text-field.mt-4(\n                            v-if='current.targetType === `external` || current.targetType === `externalblank`'\n                            outlined\n                            :label='$t(\"navigation.target\")'\n                            prepend-icon='mdi-near-me'\n                            v-model='current.target'\n                            hide-details\n                          )\n                          .d-flex.align-center.mt-4(v-else-if='current.targetType === \"page\"')\n                            v-btn.ml-8(\n                              color='primary'\n                              dark\n                              @click='selectPage'\n                              )\n                              v-icon(left) mdi-magnify\n                              span {{$t('admin:navigation.selectPageButton')}}\n                            .caption.ml-4.primary--text {{current.target}}\n                          v-text-field(\n                            v-else-if='current.targetType === `search`'\n                            outlined\n                            :label='$t(\"navigation.navType.searchQuery\")'\n                            prepend-icon='search'\n                            v-model='current.target'\n                          )\n                        v-divider\n\n                      template(v-else-if='current.kind === \"header\"')\n                        v-toolbar(height='56', color='teal lighten-1', flat, dark)\n                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}}\n                          v-spacer\n                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')\n                            v-icon(left) mdi-delete\n                            span {{$t('navigation.delete', { kind: $t('navigation.header') })}}\n                        v-card-text\n                          v-text-field(\n                            outlined\n                            :label='$t(\"navigation.label\")'\n                            prepend-icon='mdi-format-title'\n                            v-model='current.label'\n                          )\n                        v-divider\n\n                      div(v-else-if='current.kind === \"divider\"')\n                        v-toolbar(height='56', color='teal lighten-1', flat, dark)\n                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.divider') })}}\n                          v-spacer\n                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')\n                            v-icon(left) mdi-delete\n                            span {{$t('navigation.delete', { kind: $t('navigation.divider') })}}\n\n                      v-card-text(v-if='current.kind')\n                        v-radio-group.pl-8(v-model='current.visibilityMode', mandatory, hide-details)\n                          v-radio(:label='$t(\"admin:navigation.visibilityMode.all\")', value='all', color='primary')\n                          v-radio.mt-3(:label='$t(\"admin:navigation.visibilityMode.restricted\")', value='restricted', color='primary')\n                        .pl-8\n                          v-select.pl-8.mt-3(\n                            item-text='name'\n                            item-value='id'\n                            outlined\n                            prepend-icon='mdi-account-group'\n                            label='Groups'\n                            :disabled='current.visibilityMode !== `restricted`'\n                            v-model='current.visibilityGroups'\n                            :items='groups'\n                            persistent-hint\n                            clearable\n                            multiple\n                          )\n                      template(v-else)\n                        v-toolbar(height='56', color='teal lighten-1', flat, dark)\n                        v-card-text.grey--text(v-if='currentTree.length > 0') {{$t('navigation.noSelectionText')}}\n                        v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}}\n\n    v-dialog(v-model='copyFromLocaleDialogIsShown', max-width='650', persistent)\n      v-card\n        .dialog-header.is-short.is-teal\n          v-icon.mr-3(color='white') mdi-arrange-send-backward\n          span {{$t('admin:navigation.copyFromLocale')}}\n        v-card-text.pt-5\n          .body-2 {{$t('admin:navigation.copyFromLocaleInfoText')}}\n          v-select.mt-3(\n            :items='locales'\n            item-text='nativeName'\n            item-value='code'\n            outlined\n            prepend-icon='mdi-web'\n            v-model='copyFromLocaleCode'\n            :label='$t(`admin:navigation.sourceLocale`)'\n            :hint='$t(`admin:navigation.sourceLocaleHint`)'\n            persistent-hint\n            )\n        v-card-chin\n          v-spacer\n          v-btn(text, @click='copyFromLocaleDialogIsShown = false') {{$t('common:actions.cancel')}}\n          v-btn.px-3(depressed, color='primary', @click='copyFromLocale')\n            v-icon(left) mdi-chevron-right\n            span {{$t('common:actions.copy')}}\n\n    page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { v4 as uuid } from 'uuid'\n\nimport groupsQuery from 'gql/admin/users/users-query-groups.gql'\n\nimport draggable from 'vuedraggable'\n\n/* global siteConfig, siteLangs */\n\nexport default {\n  components: {\n    draggable\n  },\n  data() {\n    return {\n      selectPageModal: false,\n      trees: [],\n      current: {},\n      currentLang: siteConfig.lang,\n      groups: [],\n      copyFromLocaleDialogIsShown: false,\n      config: {\n        mode: 'NONE'\n      },\n      allLocales: [],\n      copyFromLocaleCode: 'en'\n    }\n  },\n  computed: {\n    navTypes () {\n      return [\n        { text: this.$t('navigation.navType.external'), value: 'external' },\n        { text: this.$t('navigation.navType.externalblank'), value: 'externalblank' },\n        { text: this.$t('navigation.navType.home'), value: 'home' },\n        { text: this.$t('navigation.navType.page'), value: 'page' }\n        // { text: this.$t('navigation.navType.searchQuery'), value: 'search' }\n      ]\n    },\n    locales () {\n      return _.intersectionBy(this.allLocales, _.unionBy(siteLangs, [{ code: 'en' }, { code: siteConfig.lang }], 'code'), 'code')\n    },\n    currentTree: {\n      get () {\n        return _.get(_.find(this.trees, ['locale', this.currentLang]), 'items', null) || []\n      },\n      set (val) {\n        const tree = _.find(this.trees, ['locale', this.currentLang])\n        if (tree) {\n          tree.items = val\n        } else {\n          this.trees = [...this.trees, {\n            locale: this.currentLang,\n            items: val\n          }]\n        }\n      }\n    }\n  },\n  watch: {\n    currentLang (newValue, oldValue) {\n      this.$nextTick(() => {\n        if (this.currentTree.length > 0) {\n          this.current = this.currentTree[0]\n        } else {\n          this.current = {}\n        }\n      })\n    }\n  },\n  methods: {\n    addItem(kind) {\n      let newItem = {\n        id: uuid(),\n        kind,\n        visibilityMode: 'all',\n        visibilityGroups: []\n      }\n      switch (kind) {\n        case 'link':\n          newItem = {\n            ...newItem,\n            label: this.$t('navigation.untitled', { kind: this.$t(`navigation.link`) }),\n            icon: 'mdi-chevron-right',\n            targetType: 'home',\n            target: ''\n          }\n          break\n        case 'header':\n          newItem.label = this.$t('navigation.untitled', { kind: this.$t(`navigation.header`) })\n          break\n      }\n      this.currentTree = [...this.currentTree, newItem]\n      this.current = newItem\n    },\n    deleteItem(item) {\n      this.currentTree = _.pull(this.currentTree, item)\n      this.current = {}\n    },\n    selectItem(item) {\n      this.current = item\n    },\n    selectPage() {\n      this.selectPageModal = true\n    },\n    selectPageHandle ({ path, locale }) {\n      this.current.target = `/${locale}/${path}`\n    },\n    copyFromLocale () {\n      this.copyFromLocaleDialogIsShown = false\n      this.currentTree = [...this.currentTree, ..._.get(_.find(this.trees, ['locale', this.copyFromLocaleCode]), 'items', null) || []]\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-navigation-save')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($tree: [NavigationTreeInput]!, $mode: NavigationMode!) {\n              navigation{\n                updateTree(tree: $tree) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                },\n                updateConfig(mode: $mode) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            tree: this.trees,\n            mode: this.config.mode\n          }\n        })\n        if (_.get(resp, 'data.navigation.updateTree.responseResult.succeeded', false) && _.get(resp, 'data.navigation.updateConfig.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('navigation.saveSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(_.get(resp, 'data.navigation.updateTree.responseResult.message', 'An unexpected error occurred.'))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-navigation-save')\n    },\n    async refresh() {\n      await this.$apollo.queries.trees.refetch()\n      this.current = {}\n      this.$store.commit('showNotification', {\n        message: 'Navigation has been refreshed.',\n        style: 'success',\n        icon: 'cached'\n      })\n    }\n  },\n  apollo: {\n    config: {\n      query: gql`\n        {\n          navigation {\n            config {\n              mode\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.navigation.config),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-config')\n      }\n    },\n    trees: {\n      query: gql`\n        {\n          navigation {\n            tree {\n              locale\n              items {\n                id\n                kind\n                label\n                icon\n                targetType\n                target\n                visibilityMode\n                visibilityGroups\n              }\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.navigation.tree),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-tree')\n      }\n    },\n    groups: {\n      query: groupsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-groups')\n      }\n    },\n    allLocales: {\n      query: gql`\n        {\n          localization {\n            locales {\n              code\n              name\n              nativeName\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.localization.locales,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-locales')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss' scoped>\n\n.clickable {\n  cursor: pointer;\n\n  &:hover {\n    background-color: rgba(mc('blue', '500'), .25);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-pages-edit.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap, v-if='page.id')\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-view-details.svg', alt='Edit Page', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2.animated.fadeInLeft Page Details\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s\n              v-chip.ml-0.mr-2(label, small).caption ID {{page.id}}\n              span /{{page.locale}}/{{page.path}}\n          v-spacer\n          template(v-if='page.isPublished')\n            status-indicator.mr-3(positive, pulse)\n            .caption.green--text {{$t('common:page.published')}}\n          template(v-else)\n            status-indicator.mr-3(negative, pulse)\n            .caption.red--text {{$t('common:page.unpublished')}}\n          template(v-if='page.isPrivate')\n            status-indicator.mr-3.ml-4(intermediary, pulse)\n            .caption.deep-orange--text {{$t('common:page.private')}}\n          template(v-else)\n            status-indicator.mr-3.ml-4(active, pulse)\n            .caption.blue--text {{$t('common:page.global')}}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(color='grey', icon, outlined, to='/pages')\n            v-icon mdi-arrow-left\n          v-menu(offset-y, origin='top right')\n            template(v-slot:activator='{ on }')\n              v-btn.mx-3.animated.fadeInDown.wait-p2s(color='black', v-on='on', depressed, dark)\n                span Actions\n                v-icon(right) mdi-chevron-down\n            v-list(dense, nav)\n              v-list-item(:href='`/` + page.locale + `/` + page.path')\n                v-list-item-icon\n                  v-icon(color='indigo') mdi-text-subject\n                v-list-item-title View\n              v-list-item(:href='`/e/` + page.locale + `/` + page.path')\n                v-list-item-icon\n                  v-icon(color='indigo') mdi-pencil\n                v-list-item-title Edit\n              //- v-list-item(@click='', disabled)\n              //-   v-list-item-icon\n              //-     v-icon(color='grey') mdi-cube-scan\n              //-   v-list-item-title Re-Render\n              //- v-list-item(@click='', disabled)\n              //-   v-list-item-icon\n              //-     v-icon(color='grey') mdi-earth-remove\n              //-   v-list-item-title Unpublish\n              v-list-item(:href='`/s/` + page.locale + `/` + page.path')\n                v-list-item-icon\n                  v-icon(color='indigo') mdi-code-tags\n                v-list-item-title View Source\n              v-list-item(:href='`/h/` + page.locale + `/` + page.path')\n                v-list-item-icon\n                  v-icon(color='indigo') mdi-history\n                v-list-item-title View History\n              //- v-list-item(@click='', disabled)\n              //-   v-list-item-icon\n              //-     v-icon(color='grey') mdi-content-duplicate\n              //-   v-list-item-title Duplicate\n              //- v-list-item(@click='', disabled)\n              //-   v-list-item-icon\n              //-     v-icon(color='grey') mdi-content-save-move-outline\n              //-   v-list-item-title Move / Rename\n              v-dialog(v-model='deletePageDialog', max-width='500')\n                template(v-slot:activator='{ on }')\n                  v-list-item(v-on='on')\n                    v-list-item-icon\n                      v-icon(color='red') mdi-trash-can-outline\n                    v-list-item-title Delete\n                v-card\n                  .dialog-header.is-short.is-red\n                    v-icon.mr-2(color='white') mdi-file-document-box-remove-outline\n                    span {{$t('common:page.delete')}}\n                  v-card-text.pt-5\n                    i18next.body-2(path='common:page.deleteTitle', tag='div')\n                      span.red--text.text--darken-2(place='title') {{page.title}}\n                    .caption {{$t('common:page.deleteSubtitle')}}\n                    v-chip.mt-3.ml-0.mr-1(label, color='red lighten-4', disabled, small)\n                      .caption.red--text.text--darken-2 {{page.locale.toUpperCase()}}\n                    v-chip.mt-3.mx-0(label, color='red lighten-5', disabled, small)\n                      span.red--text.text--darken-2 /{{page.path}}\n                  v-card-chin\n                    v-spacer\n                    v-btn(text, @click='deletePageDialog = false', :disabled='loading') {{$t('common:actions.cancel')}}\n                    v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}\n          v-btn.animated.fadeInDown(color='success', large, depressed, disabled)\n            v-icon(left) mdi-check\n            span Save Changes\n      v-flex(xs12, lg6)\n        v-card.animated.fadeInUp\n          v-toolbar(color='primary', dense, dark, flat)\n            v-icon.mr-2 mdi-text-subject\n            span Properties\n          v-list.py-0(two-line, dense)\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Title\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.title }}\n            v-divider\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Description\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.description || '-' }}\n            v-divider\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Locale\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.locale }}\n            v-divider\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Path\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.path }}\n            v-divider\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Editor\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.editor || '?' }}\n            v-divider\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Content Type\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.contentType || '?' }}\n            v-divider\n            v-list-item\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Page Hash\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.hash }}\n\n      v-flex(xs12, lg6)\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, dark, flat)\n            v-icon.mr-2 mdi-account-multiple\n            span Users\n          v-list.py-0(two-line, dense)\n            v-list-item\n              v-list-item-avatar(size='24')\n                v-btn(icon, :to='`/users/` + page.creatorId')\n                  v-icon(color='grey') mdi-account\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Creator\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.creatorName }} #[em.caption ({{ page.creatorEmail }})]\n              v-list-item-action\n                v-list-item-action-text {{ page.createdAt | moment('calendar') }}\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='24')\n                v-btn(icon, :to='`/users/` + page.authorId')\n                  v-icon(color='grey') mdi-account\n              v-list-item-content\n                v-list-item-title: .overline.grey--text Last Editor\n                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.authorName }} #[em.caption ({{ page.authorEmail }})]\n              v-list-item-action\n                v-list-item-action-text {{ page.updatedAt | moment('calendar') }}\n\n    v-layout(row, align-center, v-else)\n      v-progress-circular(indeterminate, width='2', color='grey')\n      .body-2.pl-3.grey--text {{ $t('common:page.loading') }}\n\n</template>\n<script>\nimport _ from 'lodash'\nimport { StatusIndicator } from 'vue-status-indicator'\n\nimport pageQuery from 'gql/admin/pages/pages-query-single.gql'\nimport deletePageMutation from 'gql/common/common-pages-mutation-delete.gql'\n\nexport default {\n  components: {\n    StatusIndicator\n  },\n  data() {\n    return {\n      deletePageDialog: false,\n      page: {},\n      loading: false\n    }\n  },\n  methods: {\n    async deletePage() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'page-delete')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: deletePageMutation,\n          variables: {\n            id: this.page.id\n          }\n        })\n        if (_.get(resp, 'data.pages.delete.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'green',\n            message: `Page deleted successfully.`,\n            icon: 'check'\n          })\n          this.$router.replace('/pages')\n        } else {\n          throw new Error(_.get(resp, 'data.pages.delete.responseResult.message', this.$t('common:error.unexpected')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'page-delete')\n    },\n    async rerenderPage() {\n      this.$store.commit('showNotification', {\n        style: 'indigo',\n        message: `Coming soon...`,\n        icon: 'directions_boat'\n      })\n    }\n  },\n  apollo: {\n    page: {\n      query: pageQuery,\n      variables() {\n        return {\n          id: _.toSafeInteger(this.$route.params.id)\n        }\n      },\n      fetchPolicy: 'network-only',\n      update: (data) => data.pages.single,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-pages-visualize.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2.animated.fadeInLeft Visualize Pages\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Dendrogram representation of your pages\n          v-spacer\n          v-select.mx-5.animated.fadeInDown.wait-p1s(\n            v-if='locales.length > 0'\n            v-model='currentLocale'\n            :items='locales'\n            style='flex: 0 1 120px;'\n            solo\n            dense\n            hide-details\n            item-value='code'\n            item-text='name'\n          )\n          v-btn-toggle.animated.fadeInDown(v-model='graphMode', color='primary', dense, rounded)\n            v-btn.px-5(value='htree')\n              v-icon(left, :color='graphMode === `htree` ? `primary` : `grey darken-3`') mdi-sitemap\n              span.text-none Hierarchical Tree\n            v-btn.px-5(value='hradial')\n              v-icon(left, :color='graphMode === `hradial` ? `primary` : `grey darken-3`') mdi-chart-donut-variant\n              span.text-none Hierarchical Radial\n            v-btn.px-5(value='rradial')\n              v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial\n              span.text-none Relational Radial\n        .admin-pages-visualize-svg(ref='svgContainer', v-show='pages.length >= 1')\n        v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!\n</template>\n\n<script>\nimport _ from 'lodash'\nimport * as d3 from 'd3'\nimport gql from 'graphql-tag'\n\n/* global siteConfig, siteLangs */\n\nexport default {\n  data() {\n    return {\n      graphMode: 'htree',\n      width: 800,\n      radius: 400,\n      pages: [],\n      locales: siteLangs,\n      currentLocale: siteConfig.lang\n    }\n  },\n  watch: {\n    pages () {\n      this.redraw()\n    },\n    graphMode () {\n      this.redraw()\n    }\n  },\n  methods: {\n    goToPage (d) {\n      const id = d.data.id\n      if (id) {\n        if (d3.event.ctrlKey || d3.event.metaKey) {\n          const { href } = this.$router.resolve(String(id))\n          window.open(href, '_blank')\n        } else {\n          this.$router.push(String(id))\n        }\n      }\n    },\n    bilink (root) {\n      const map = new Map(root.descendants().map(d => [d.data.path, d]))\n      for (const d of root.descendants()) {\n        d.incoming = []\n        d.outgoing = []\n        d.data.links.forEach(i => {\n          const relNode = map.get(i)\n          if (relNode) {\n            d.outgoing.push([d, relNode])\n          }\n        })\n      }\n      for (const d of root.descendants()) {\n        for (const o of d.outgoing) {\n          if (o[1]) {\n            o[1].incoming.push(o)\n          }\n        }\n      }\n      return root\n    },\n    hierarchy (pages) {\n      const map = new Map(pages.map(p => [p.path, p]))\n      const getPage = path => map.get(path) || {\n        path: path,\n        title: path.split('/').slice(-1)[0],\n        links: []\n      }\n\n      function recurse (depth, [parent, descendants]) {\n        const truncatePath = path => _.take(path.split('/'), depth).join('/')\n        const descendantsByChild =\n          Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))\n            .map(([childPath, descendantsGroup]) => [getPage(childPath), _.sortBy(descendantsGroup, child => child.path)])\n            .map(([child, descendantsGroup]) =>\n              [child, _.filter(descendantsGroup, d => d.path !== child.path)])\n        return {\n          ...parent,\n          children: descendantsByChild.map(_.partial(recurse, depth + 1))\n        }\n      }\n      const root = { path: this.currentLocale, title: this.currentLocale, links: [] }\n      // start at depth=2 because we're taking {locale} as the root and\n      // all paths start with {locale}/\n      return recurse(2, [root, pages])\n    },\n    /**\n     * Relational Radial\n     */\n    drawRelations () {\n      const data = this.hierarchy(this.pages)\n\n      const line = d3.lineRadial()\n        .curve(d3.curveBundle.beta(0.85))\n        .radius(d => d.y)\n        .angle(d => d.x)\n\n      const tree = d3.cluster()\n        .size([2 * Math.PI, this.radius - 100])\n\n      const root = tree(this.bilink(d3.hierarchy(data)\n        .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.path, b.data.path))))\n\n      const svg = d3.create('svg')\n        .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])\n\n      const g = svg.append('g')\n\n      svg.call(d3.zoom().on('zoom', function() {\n        g.attr('transform', d3.event.transform)\n      }))\n\n      const link = g.append('g')\n        .attr('stroke', '#CCC')\n        .attr('fill', 'none')\n        .selectAll('path')\n        .data(root.descendants().flatMap(leaf => leaf.outgoing))\n        .join('path')\n        .style('mix-blend-mode', 'multiply')\n        .attr('d', ([i, o]) => line(i.path(o)))\n        .each(function(d) { d.path = this })\n\n      g.append('g')\n        .attr('font-family', 'sans-serif')\n        .attr('font-size', 10)\n        .selectAll('g')\n        .data(root.descendants())\n        .join('g')\n        .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)\n        .append('text')\n        .attr('dy', '0.31em')\n        .attr('x', d => d.x < Math.PI ? 6 : -6)\n        .attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')\n        .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)\n        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')\n        .attr('cursor', 'pointer')\n        .text(d => d.data.title)\n        .each(function(d) { d.text = this })\n        .on('mouseover', overed)\n        .on('mouseout', outed)\n        .on('click', d => this.goToPage(d))\n        .call(text => text.append('title').text(d => `${d.data.path}\n          ${d.outgoing.length} outgoing\n          ${d.incoming.length} incoming`))\n        .clone(true).lower()\n        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')\n\n      function overed(d) {\n        link.style('mix-blend-mode', null)\n        d3.select(this).attr('font-weight', 'bold')\n        d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', '#2196F3').raise()\n        d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', '#2196F3').attr('font-weight', 'bold')\n        d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', '#E91E63').raise()\n        d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', '#E91E63').attr('font-weight', 'bold')\n      }\n\n      function outed(d) {\n        link.style('mix-blend-mode', 'multiply')\n        d3.select(this).attr('font-weight', null)\n        d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', null)\n        d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', null).attr('font-weight', null)\n        d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', null)\n        d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', null).attr('font-weight', null)\n      }\n\n      this.$refs.svgContainer.appendChild(svg.node())\n    },\n    /**\n     * Hierarchical Tree\n     */\n    drawTree () {\n      const data = this.hierarchy(this.pages)\n\n      const treeRoot = d3.hierarchy(data)\n      treeRoot.dx = 10\n      treeRoot.dy = this.width / (treeRoot.height + 1)\n      const root = d3.tree().nodeSize([treeRoot.dx, treeRoot.dy])(treeRoot)\n\n      let x0 = Infinity\n      let x1 = -x0\n      root.each(d => {\n        if (d.x > x1) x1 = d.x\n        if (d.x < x0) x0 = d.x\n      })\n\n      const svg = d3.create('svg')\n        .attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])\n\n      // this extra level is necessary because the element that we\n      // apply the zoom tranform to must be above the element where\n      // we apply the translation (`g`), or else zoom is wonky\n      const gZoom = svg.append('g')\n\n      svg.call(d3.zoom().on('zoom', function() {\n        gZoom.attr('transform', d3.event.transform)\n      }))\n\n      const g = gZoom.append('g')\n        .attr('font-family', 'sans-serif')\n        .attr('font-size', 10)\n        .attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)\n\n      g.append('g')\n        .attr('fill', 'none')\n        .attr('stroke', this.$vuetify.theme.dark ? '#999' : '#555')\n        .attr('stroke-opacity', 0.4)\n        .attr('stroke-width', 1.5)\n        .selectAll('path')\n        .data(root.links())\n        .join('path')\n        .attr('d', d3.linkHorizontal()\n          .x(d => d.y)\n          .y(d => d.x))\n\n      const node = g.append('g')\n        .attr('stroke-linejoin', 'round')\n        .attr('stroke-width', 3)\n        .selectAll('g')\n        .data(root.descendants())\n        .join('g')\n        .attr('transform', d => `translate(${d.y},${d.x})`)\n\n      node.append('circle')\n        .attr('fill', d => d.children ? '#555' : '#999')\n        .attr('r', 2.5)\n\n      node.append('text')\n        .attr('dy', '0.31em')\n        .attr('x', d => d.children ? -6 : 6)\n        .attr('text-anchor', d => d.children ? 'end' : 'start')\n        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')\n        .attr('cursor', 'pointer')\n        .text(d => d.data.title)\n        .on('click', d => this.goToPage(d))\n        .clone(true).lower()\n        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')\n\n      this.$refs.svgContainer.appendChild(svg.node())\n    },\n    /**\n     * Hierarchical Radial\n     */\n    drawRadialTree () {\n      const data = this.hierarchy(this.pages)\n\n      const tree = d3.tree()\n        .size([2 * Math.PI, this.radius])\n        .separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth)\n\n      const root = tree(d3.hierarchy(data)\n        .sort((a, b) => d3.ascending(a.data.title, b.data.title)))\n\n      const svg = d3.create('svg')\n        .style('font', '10px sans-serif')\n\n      const g = svg.append('g')\n\n      svg.call(d3.zoom().on('zoom', function () {\n        g.attr('transform', d3.event.transform)\n      }))\n\n      // eslint-disable-next-line no-unused-vars\n      const link = g.append('g')\n        .attr('fill', 'none')\n        .attr('stroke', this.$vuetify.theme.dark ? 'white' : '#555')\n        .attr('stroke-opacity', 0.4)\n        .attr('stroke-width', 1.5)\n        .selectAll('path')\n        .data(root.links())\n        .join('path')\n        .attr('d', d3.linkRadial()\n          .angle(d => d.x)\n          .radius(d => d.y))\n\n      const node = g.append('g')\n        .attr('stroke-linejoin', 'round')\n        .attr('stroke-width', 3)\n        .selectAll('g')\n        .data(root.descendants().reverse())\n        .join('g')\n        .attr('transform', d => `\n          rotate(${d.x * 180 / Math.PI - 90})\n          translate(${d.y},0)\n        `)\n\n      node.append('circle')\n        .attr('fill', d => d.children ? '#555' : '#999')\n        .attr('r', 2.5)\n\n      node.append('text')\n        .attr('dy', '0.31em')\n        /* eslint-disable no-mixed-operators */\n        .attr('x', d => d.x < Math.PI === !d.children ? 6 : -6)\n        .attr('text-anchor', d => d.x < Math.PI === !d.children ? 'start' : 'end')\n        .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)\n        /* eslint-enable no-mixed-operators */\n        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')\n        .attr('cursor', 'pointer')\n        .text(d => d.data.title)\n        .on('click', d => this.goToPage(d))\n        .clone(true).lower()\n        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')\n\n      this.$refs.svgContainer.appendChild(svg.node())\n\n      function autoBox() {\n        const {x, y, width, height} = this.getBBox()\n        return [x, y, width, height]\n      }\n\n      svg.attr('viewBox', autoBox)\n    },\n    redraw () {\n      while (this.$refs.svgContainer.firstChild) {\n        this.$refs.svgContainer.firstChild.remove()\n      }\n      if (this.pages.length > 0) {\n        switch (this.graphMode) {\n          case 'rradial':\n            this.drawRelations()\n            break\n          case 'htree':\n            this.drawTree()\n            break\n          case 'hradial':\n            this.drawRadialTree()\n            break\n        }\n      }\n    }\n  },\n  apollo: {\n    pages: {\n      query: gql`\n        query ($locale: String!) {\n          pages {\n            links(locale: $locale) {\n              id\n              path\n              title\n              links\n            }\n          }\n        }\n      `,\n      variables () {\n        return {\n          locale: this.currentLocale\n        }\n      },\n      fetchPolicy: 'network-only',\n      update: (data) => data.pages.links,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.admin-pages-visualize-svg {\n  text-align: center;\n  // 100vh - header - title section - footer - content padding\n  height: calc(100vh - 64px - 92px - 32px - 16px);\n\n  > svg {\n    height: 100%;\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-pages.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Page', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2.animated.fadeInLeft Pages\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage pages\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p1s(icon, color='grey', outlined, @click='refresh')\n            v-icon.grey--text mdi-refresh\n          //- v-btn.animated.fadeInDown.mx-3(color='primary', outlined, @click='recyclebin', disabled)\n          //-   v-icon(left) mdi-delete-outline\n          //-   span Recycle Bin\n          v-btn.animated.fadeInDown(color='primary', depressed, large, to='pages/visualize')\n            v-icon(left) mdi-graph\n            span Visualize\n        v-card.mt-3.animated.fadeInUp\n          .pa-2.d-flex.align-center(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-3`')\n            v-text-field(\n              solo\n              flat\n              v-model='search'\n              prepend-inner-icon='mdi-file-search-outline'\n              label='Search Pages...'\n              hide-details\n              dense\n              style='max-width: 400px;'\n              )\n            v-spacer\n            v-select.ml-2(\n              solo\n              flat\n              hide-details\n              dense\n              label='Locale'\n              :items='langs'\n              v-model='selectedLang'\n              style='max-width: 250px;'\n            )\n            v-select.ml-2(\n              solo\n              flat\n              hide-details\n              dense\n              label='Publish State'\n              :items='states'\n              v-model='selectedState'\n              style='max-width: 250px;'\n            )\n          v-divider\n          v-data-table(\n            :items='filteredPages'\n            :headers='headers'\n            :search='search'\n            :page.sync='pagination'\n            :items-per-page='15'\n            :loading='loading'\n            must-sort,\n            sort-by='updatedAt',\n            sort-desc,\n            hide-default-footer\n            @page-count=\"pageTotal = $event\"\n          )\n            template(slot='item', slot-scope='props')\n              tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')\n                td.text-xs-right {{ props.item.id }}\n                td\n                  .body-2: strong {{ props.item.title }}\n                  .caption {{ props.item.description }}\n                td.admin-pages-path\n                  v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}\n                  span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}\n                td {{ props.item.createdAt | moment('calendar') }}\n                td {{ props.item.updatedAt | moment('calendar') }}\n            template(slot='no-data')\n              v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.\n          .text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')\n            v-pagination(v-model='pagination', :length='pageTotal')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport pagesQuery from 'gql/admin/pages/pages-query-list.gql'\n\nexport default {\n  data() {\n    return {\n      selectedPage: {},\n      pagination: 1,\n      pages: [],\n      pageTotal: 0,\n      headers: [\n        { text: 'ID', value: 'id', width: 80, sortable: true },\n        { text: 'Title', value: 'title' },\n        { text: 'Path', value: 'path' },\n        { text: 'Created', value: 'createdAt', width: 250 },\n        { text: 'Last Updated', value: 'updatedAt', width: 250 }\n      ],\n      search: '',\n      selectedLang: null,\n      selectedState: null,\n      states: [\n        { text: 'All Publishing States', value: null },\n        { text: 'Published', value: true },\n        { text: 'Not Published', value: false }\n      ],\n      loading: false\n    }\n  },\n  computed: {\n    filteredPages () {\n      return _.filter(this.pages, pg => {\n        if (this.selectedLang !== null && this.selectedLang !== pg.locale) {\n          return false\n        }\n        if (this.selectedState !== null && this.selectedState !== pg.isPublished) {\n          return false\n        }\n        return true\n      })\n    },\n    langs () {\n      return _.concat({\n        text: 'All Locales',\n        value: null\n      }, _.uniqBy(this.pages, 'locale').map(pg => ({\n        text: pg.locale,\n        value: pg.locale\n      })))\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.pages.refetch()\n      this.$store.commit('showNotification', {\n        message: 'Page list has been refreshed.',\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    newpage() {\n      this.pageSelectorShown = true\n    },\n    recyclebin () { }\n  },\n  apollo: {\n    pages: {\n      query: pagesQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.pages.list,\n      watchLoading (isLoading) {\n        this.loading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.admin-pages-path {\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  font-family: 'Roboto Mono', monospace;\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-rendering.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-process.svg', alt='Rendering', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:rendering.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:rendering.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/rendering', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n      v-flex.animated.fadeInUp(lg3, xs12)\n        v-toolbar(\n          color='blue darken-2'\n          dense\n          flat\n          dark\n          )\n          .subtitle-1 Pipeline\n        v-expansion-panels.adm-rendering-pipeline(\n          v-model='selectedCore'\n          accordion\n          mandatory\n          )\n          v-expansion-panel(\n            v-for='core in renderers'\n            :key='core.key'\n            )\n            v-expansion-panel-header(\n              hide-actions\n              ripple\n            )\n              v-toolbar(\n                color='blue'\n                dense\n                dark\n                flat\n                )\n                v-spacer\n                .body-2 {{core.input}}\n                v-icon.mx-2 mdi-arrow-right-circle\n                .caption {{core.output}}\n                v-spacer\n            v-expansion-panel-content\n              v-list.py-0(two-line, dense)\n                template(v-for='(rdr, n) in core.children')\n                  v-list-item(\n                    :key='rdr.key'\n                    @click='selectRenderer(rdr.key)'\n                    :class='currentRenderer.key === rdr.key ? ($vuetify.theme.dark ? `grey darken-4-l4` : `blue lighten-5`) : ``'\n                    )\n                    v-list-item-avatar(size='24', tile)\n                      v-icon(:color='currentRenderer.key === rdr.key ? \"primary\" : \"grey\"') {{rdr.icon}}\n                    v-list-item-content\n                      v-list-item-title {{rdr.title}}\n                      v-list-item-subtitle: .caption {{rdr.description}}\n                    v-list-item-avatar(size='24')\n                      status-indicator(v-if='rdr.isEnabled', positive, pulse)\n                      status-indicator(v-else, negative, pulse)\n                  v-divider.my-0(v-if='n < core.children.length - 1')\n\n      v-flex(lg9, xs12)\n        v-card.wiki-form.animated.fadeInUp\n          v-toolbar(\n            color='indigo'\n            dark\n            flat\n            dense\n            )\n            v-icon.mr-2 {{currentRenderer.icon}}\n            .subtitle-1 {{currentRenderer.title}}\n            v-spacer\n            v-switch(\n              dark\n              color='white'\n              label='Enabled'\n              v-model='currentRenderer.isEnabled'\n              hide-details\n              inset\n              )\n          v-card-info(color='blue')\n            div\n              div {{currentRenderer.description}}\n              span.caption: a(href='https://docs.requarks.io/en/rendering', target='_blank') Documentation\n          v-card-text.pb-4.pl-4\n            .overline.mb-5 Rendering Module Configuration\n            .body-2.ml-3(v-if='!currentRenderer.config || currentRenderer.config.length < 1'): em This rendering module has no configuration options you can modify.\n            template(v-else, v-for='(cfg, idx) in currentRenderer.config')\n              v-select(\n                v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                outlined\n                :items='cfg.value.enum'\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                color='indigo'\n              )\n              v-switch(\n                v-else-if='cfg.value.type === \"boolean\"'\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                color='indigo'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                inset\n                )\n              v-text-field(\n                v-else\n                outlined\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                color='indigo'\n                )\n              v-divider.my-5(v-if='idx < currentRenderer.config.length - 1')\n          v-card-chin\n            v-spacer\n            .caption.pr-3.grey--text Module: {{ currentRenderer.key }}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { DepGraph } from 'dependency-graph'\n\nimport { StatusIndicator } from 'vue-status-indicator'\n\nimport renderersQuery from 'gql/admin/rendering/rendering-query-renderers.gql'\nimport renderersSaveMutation from 'gql/admin/rendering/rendering-mutation-save-renderers.gql'\n\nexport default {\n  components: {\n    StatusIndicator\n  },\n  data() {\n    return {\n      selectedCore: -1,\n      renderers: [],\n      currentRenderer: {}\n    }\n  },\n  watch: {\n    renderers(newValue, oldValue) {\n      _.delay(() => {\n        this.selectedCore = _.findIndex(newValue, ['key', 'markdownCore'])\n        this.selectRenderer('markdownCore')\n      }, 500)\n    }\n  },\n  methods: {\n    selectRenderer (key) {\n      this.renderers.map(rdr => {\n        if (_.some(rdr.children, ['key', key])) {\n          this.currentRenderer = _.find(rdr.children, ['key', key])\n        }\n      })\n    },\n    async refresh () {\n      await this.$apollo.queries.renderers.refetch()\n      this.$store.commit('showNotification', {\n        message: 'Rendering active configuration has been reloaded.',\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async save () {\n      this.$store.commit(`loadingStart`, 'admin-rendering-saverenderers')\n      await this.$apollo.mutate({\n        mutation: renderersSaveMutation,\n        variables: {\n          renderers: _.reduce(this.renderers, (result, core) => {\n            result = _.concat(result, core.children.map(rd => ({\n              key: rd.key,\n              isEnabled: rd.isEnabled,\n              config: rd.config.map(cfg => ({ key: cfg.key, value: JSON.stringify({ v: cfg.value.value }) }))\n            })))\n            return result\n          }, [])\n        }\n      })\n      this.$store.commit('showNotification', {\n        message: 'Rendering configuration saved successfully.',\n        style: 'success',\n        icon: 'check'\n      })\n      this.$store.commit(`loadingStop`, 'admin-rendering-saverenderers')\n    }\n  },\n  apollo: {\n    renderers: {\n      query: renderersQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => {\n        let renderers = _.cloneDeep(data.rendering.renderers).map(str => ({\n          ...str,\n          config: _.sortBy(str.config.map(cfg => ({\n            ...cfg,\n            value: JSON.parse(cfg.value)\n          })), [t => t.value.order])\n        }))\n        // Build tree\n        const graph = new DepGraph({ circular: true })\n        const rawCores = _.filter(renderers, ['dependsOn', null]).map(core => {\n          core.children = _.concat([_.cloneDeep(core)], _.filter(renderers, ['dependsOn', core.key]))\n          return core\n        })\n        // Build dependency graph\n        rawCores.map(core => { graph.addNode(core.key) })\n        rawCores.map(core => {\n          rawCores.map(coreTarget => {\n            if (core.key !== coreTarget.key) {\n              if (core.output === coreTarget.input) {\n                graph.addDependency(core.key, coreTarget.key)\n              }\n            }\n          })\n        })\n        // Reorder cores in reverse dependency order\n        let orderedCores = []\n        _.reverse(graph.overallOrder()).map(coreKey => {\n          orderedCores.push(_.find(rawCores, ['key', coreKey]))\n        })\n        return orderedCores\n      },\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-rendering-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.adm-rendering-pipeline {\n  .v-expansion-panel--active .v-expansion-panel-header {\n    min-height: 0;\n  }\n\n  .v-expansion-panel-header {\n    padding: 0;\n    margin-top: 1px;\n  }\n\n  .v-expansion-panel-content__wrap {\n    padding: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-search.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('admin:search.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:search.subtitle')}}\n          v-spacer\n          v-btn.mr-3.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/search', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.mx-3.animated.fadeInDown.wait-p1s(color='black', dark, depressed, @click='rebuild')\n            v-icon(left) mdi-cached\n            span {{$t('admin:search.rebuildIndex')}}\n          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='primary', dark, dense)\n            .subtitle-1 {{$t('admin:search.searchEngine')}}\n          v-list.py-0(two-line, dense)\n            template(v-for='(eng, idx) in engines')\n              v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')\n                v-list-item-avatar(size='24')\n                  v-icon(color='grey', v-if='!eng.isAvailable') mdi-minus-box-outline\n                  v-icon(color='primary', v-else-if='eng.key === selectedEngine') mdi-checkbox-marked-circle-outline\n                  v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline\n                v-list-item-content\n                  v-list-item-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}\n                  v-list-item-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}\n                v-list-item-avatar(v-if='selectedEngine === eng.key', size='24')\n                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right\n              v-divider(v-if='idx < engines.length - 1')\n\n      v-flex(lg9, xs12)\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{engine.title}}\n          v-card-info(color='blue')\n            div\n              div {{engine.description}}\n              span.caption: a(:href='engine.website') {{engine.website}}\n            v-spacer\n            .admin-providerlogo\n              img(:src='engine.logo', :alt='engine.title')\n          v-card-text\n            .overline.mb-5 {{$t('admin:search.engineConfig')}}\n            .body-2.ml-3(v-if='!engine.config || engine.config.length < 1'): em {{$t('admin:search.engineNoConfig')}}\n            template(v-else, v-for='cfg in engine.config')\n              v-select(\n                v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                outlined\n                :items='cfg.value.enum'\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n              )\n              v-switch.mb-3(\n                v-else-if='cfg.value.type === \"boolean\"'\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                color='primary'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                inset\n                )\n              v-textarea(\n                v-else-if='cfg.value.type === \"string\" && cfg.value.multiline'\n                outlined\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                )\n              v-text-field(\n                v-else\n                outlined\n                :key='cfg.key'\n                :label='cfg.value.title'\n                v-model='cfg.value.value'\n                prepend-icon='mdi-cog-box'\n                :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                persistent-hint\n                :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                )\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport enginesQuery from 'gql/admin/search/search-query-engines.gql'\nimport enginesSaveMutation from 'gql/admin/search/search-mutation-save-engines.gql'\nimport enginesRebuildMutation from 'gql/admin/search/search-mutation-rebuild-index.gql'\n\nexport default {\n  data() {\n    return {\n      engines: [],\n      selectedEngine: '',\n      engine: {}\n    }\n  },\n  watch: {\n    selectedEngine(newValue, oldValue) {\n      this.engine = _.find(this.engines, ['key', newValue]) || {}\n    },\n    engines(newValue, oldValue) {\n      this.selectedEngine = _.get(_.find(this.engines, 'isEnabled'), 'key', 'db')\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.engines.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('admin:search.listRefreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-search-saveengines')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: enginesSaveMutation,\n          variables: {\n            engines: this.engines.map(tgt => ({\n              isEnabled: tgt.key === this.selectedEngine,\n              key: tgt.key,\n              config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))\n            }))\n          }\n        })\n        if (_.get(resp, 'data.search.updateSearchEngines.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('admin:search.configSaveSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(_.get(resp, 'data.search.updateSearchEngines.responseResult.message', this.$t('common:error.unexpected')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-search-saveengines')\n    },\n    async rebuild () {\n      this.$store.commit(`loadingStart`, 'admin-search-rebuildindex')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: enginesRebuildMutation\n        })\n        if (_.get(resp, 'data.search.rebuildIndex.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('admin:search.indexRebuildSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(_.get(resp, 'data.search.rebuildIndex.responseResult.message', this.$t('common:error.unexpected')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-search-rebuildindex')\n    }\n  },\n  apollo: {\n    engines: {\n      query: enginesQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.search.searchEngines).map(str => ({\n        ...str,\n        config: _.sortBy(str.config.map(cfg => ({\n          ...cfg,\n          value: JSON.parse(cfg.value)\n        })), [t => t.value.order])\n      })),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-search-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss' scoped>\n\n.enginelogo {\n  width: 250px;\n  height: 85px;\n  float:right;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n\n  img {\n    max-width: 100%;\n    max-height: 50px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-security.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-private.svg', alt='Security', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:security.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:security.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp\n                v-toolbar(color='red darken-2', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 Security\n                v-card-info(color='red')\n                  span Make sure to understand the implications before turning on / off a security feature.\n                v-card-text\n                  v-switch(\n                    inset\n                    label='Block Open Redirect'\n                    color='red darken-2'\n                    v-model='config.securityOpenRedirect'\n                    persistent-hint\n                    hint='Prevents user controlled URLs from directing to websites outside of your wiki. This provides Open Redirect protection.'\n                    )\n\n                  v-divider.mt-3\n                  v-switch.mt-3(\n                    inset\n                    label='Block IFrame Embedding'\n                    color='red darken-2'\n                    v-model='config.securityIframe'\n                    persistent-hint\n                    hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'\n                    )\n\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    label='Same Origin Referrer Policy'\n                    color='red darken-2'\n                    v-model='config.securityReferrerPolicy'\n                    persistent-hint\n                    hint='Limits the referrer header to same origin.'\n                    )\n\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    label='Trust X-Forwarded-* Proxy Headers'\n                    color='red darken-2'\n                    v-model='config.securityTrustProxy'\n                    persistent-hint\n                    hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'\n                    )\n\n                  //- v-divider.mt-3\n                  //- v-switch(\n                  //-   inset\n                  //-   label='Subresource Integrity (SRI)'\n                  //-   color='red darken-2'\n                  //-   v-model='config.securitySRI'\n                  //-   persistent-hint\n                  //-   hint='This ensure that resources such as CSS and JS files are not altered during delivery.'\n                  //-   disabled\n                  //-   )\n\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    label='Enforce HSTS'\n                    color='red darken-2'\n                    v-model='config.securityHSTS'\n                    persistent-hint\n                    hint='This ensures the connection cannot be established through an insecure HTTP connection.'\n                    )\n                  v-select.mt-5(\n                    outlined\n                    label='HSTS Max Age'\n                    :items='hstsDurations'\n                    v-model='config.securityHSTSDuration'\n                    prepend-icon='mdi-subdirectory-arrow-right'\n                    :disabled='!config.securityHSTS'\n                    hide-details\n                    style='max-width: 450px;'\n                    )\n                  .pl-11.mt-3\n                    .caption Defines the duration for which the server should only deliver content through HTTPS.\n                    .caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.\n\n                  //- v-divider.mt-3\n                  //- v-switch(\n                  //-   inset\n                  //-   label='Enforce CSP'\n                  //-   color='red darken-2'\n                  //-   v-model='config.securityCSP'\n                  //-   persistent-hint\n                  //-   hint='Restricts scripts to pre-approved content sources.'\n                  //-   disabled\n                  //-   )\n                  //- v-textarea.mt-5(\n                  //-   label='CSP Directives'\n                  //-   outlined\n                  //-   v-model='config.securityCSPDirectives'\n                  //-   prepend-icon='mdi-subdirectory-arrow-right'\n                  //-   persistent-hint\n                  //-   hint='One directive per line.'\n                  //-   disabled\n                  //- )\n\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp.wait-p2s\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{ $t('admin:security.uploads') }}\n                v-card-info(color='blue')\n                  span {{$t('admin:security.uploadsInfo')}}\n                v-card-text\n                  v-text-field.mt-3(\n                    outlined\n                    :label='$t(`admin:security.maxUploadSize`)'\n                    required\n                    v-model='config.uploadMaxFileSize'\n                    prepend-icon='mdi-progress-upload'\n                    :hint='$t(`admin:security.maxUploadSizeHint`)'\n                    persistent-hint\n                    :suffix='$t(`admin:security.maxUploadSizeSuffix`)'\n                    style='max-width: 450px;'\n                    )\n                  v-text-field.mt-3(\n                    outlined\n                    :label='$t(`admin:security.maxUploadBatch`)'\n                    required\n                    v-model='config.uploadMaxFiles'\n                    prepend-icon='mdi-upload-lock'\n                    :hint='$t(`admin:security.maxUploadBatchHint`)'\n                    persistent-hint\n                    :suffix='$t(`admin:security.maxUploadBatchSuffix`)'\n                    style='max-width: 450px;'\n                    )\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    label='Scan and Sanitize SVG Uploads'\n                    color='primary'\n                    v-model='config.uploadScanSVG'\n                    persistent-hint\n                    hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.'\n                    )\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    label='Force Download of Unsafe Extensions'\n                    color='primary'\n                    v-model='config.uploadForceDownload'\n                    persistent-hint\n                    hint='Should non-image files be forced as downloads when accessed directly. This prevents potential XSS attacks via unsafe file extensions uploads.'\n                    )\n\n              v-card.mt-3.animated.fadeInUp.wait-p2s\n                v-toolbar(flat, color='primary', dark, dense)\n                  .subtitle-1 {{$t('admin:security.login')}}\n                //- v-card-info(color='blue')\n                //-   span {{$t('admin:security.loginInfo')}}\n                .overline.grey--text.pa-4 {{$t('admin:security.loginScreen')}}\n                .px-4.pb-3\n                  v-text-field(\n                    outlined\n                    :label='$t(`admin:security.loginBgUrl`)'\n                    v-model='config.authLoginBgUrl'\n                    :hint='$t(`admin:security.loginBgUrlHint`)'\n                    persistent-hint\n                    prepend-icon='mdi-image-area'\n                    append-icon='mdi-folder-image'\n                    @click:append='browseLoginBg'\n                  )\n                  v-switch(\n                    inset\n                    :label='$t(`admin:security.bypassLogin`)'\n                    color='primary'\n                    v-model='config.authAutoLogin'\n                    prepend-icon='mdi-fast-forward'\n                    persistent-hint\n                    :hint='$t(`admin:security.bypassLoginHint`)'\n                    )\n                  v-switch(\n                    inset\n                    :label='$t(`admin:security.hideLocalLogin`)'\n                    color='primary'\n                    v-model='config.authHideLocal'\n                    prepend-icon='mdi-eye-off-outline'\n                    persistent-hint\n                    :hint='$t(`admin:security.hideLocalLoginHint`)'\n                    )\n                v-divider.mt-3\n                .overline.grey--text.pa-4 {{$t('admin:security.loginSecurity')}}\n                .px-4.pb-3\n                  v-switch.mt-0(\n                    inset\n                    :label='$t(`admin:security.enforce2fa`)'\n                    color='primary'\n                    v-model='config.authEnforce2FA'\n                    prepend-icon='mdi-two-factor-authentication'\n                    :hint='$t(`admin:security.enforce2faHint`)'\n                    persistent-hint\n                  )\n                v-divider.mt-3\n                .overline.grey--text.pa-4 {{$t('admin:security.jwt')}}\n                .px-4.pb-3\n                  v-text-field(\n                    v-model='config.authJwtAudience'\n                    outlined\n                    prepend-icon='mdi-account-group-outline'\n                    :label='$t(`admin:auth.jwtAudience`)'\n                    :hint='$t(`admin:auth.jwtAudienceHint`)'\n                    persistent-hint\n                  )\n                  v-text-field.mt-3(\n                    v-model='config.authJwtExpiration'\n                    outlined\n                    prepend-icon='mdi-clock-outline'\n                    :label='$t(`admin:auth.tokenExpiration`)'\n                    :hint='$t(`admin:auth.tokenExpirationHint`)'\n                    persistent-hint\n                  )\n                  v-text-field.mt-3(\n                    v-model='config.authJwtRenewablePeriod'\n                    outlined\n                    prepend-icon='mdi-update'\n                    :label='$t(`admin:auth.tokenRenewalPeriod`)'\n                    :hint='$t(`admin:auth.tokenRenewalPeriodHint`)'\n                    persistent-hint\n                  )\n\n    component(:is='activeModal')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync } from 'vuex-pathify'\nimport gql from 'graphql-tag'\n\nimport editorStore from '../../store/editor'\n\n/* global WIKI */\n\nWIKI.$store.registerModule('editor', editorStore)\n\nexport default {\n  i18nOptions: { namespaces: 'editor' },\n  components: {\n    editorModalMedia: () => import(/* webpackChunkName: \"editor\", webpackMode: \"lazy\" */ '../editor/editor-modal-media.vue')\n  },\n  data() {\n    return {\n      config: {\n        uploadMaxFileSize: 0,\n        uploadMaxFiles: 0,\n        uploadScanSVG: true,\n        uploadForceDownload: true,\n        securityOpenRedirect: true,\n        securityIframe: true,\n        securityReferrerPolicy: true,\n        securityTrustProxy: false,\n        securitySRI: true,\n        securityHSTS: false,\n        securityHSTSDuration: 0,\n        securityCSP: false,\n        securityCSPDirectives: '',\n        authAutoLogin: false,\n        authHideLocal: false,\n        authLoginBgUrl: '',\n        authJwtAudience: 'urn:wiki.js',\n        authJwtExpiration: '30m',\n        authJwtRenewablePeriod: '14d'\n      },\n      hstsDurations: [\n        { value: 300, text: '5 minutes' },\n        { value: 86400, text: '1 day' },\n        { value: 604800, text: '1 week' },\n        { value: 2592000, text: '1 month' },\n        { value: 31536000, text: '1 year' },\n        { value: 63072000, text: '2 years' }\n      ]\n    }\n  },\n  computed: {\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n    async save () {\n      try {\n        await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $authAutoLogin: Boolean\n              $authEnforce2FA: Boolean\n              $authHideLocal: Boolean\n              $authLoginBgUrl: String\n              $authJwtAudience: String\n              $authJwtExpiration: String\n              $authJwtRenewablePeriod: String\n              $uploadMaxFileSize: Int\n              $uploadMaxFiles: Int\n              $uploadScanSVG: Boolean\n              $uploadForceDownload: Boolean\n              $securityOpenRedirect: Boolean\n              $securityIframe: Boolean\n              $securityReferrerPolicy: Boolean\n              $securityTrustProxy: Boolean\n              $securitySRI: Boolean\n              $securityHSTS: Boolean\n              $securityHSTSDuration: Int\n              $securityCSP: Boolean\n              $securityCSPDirectives: String\n            ) {\n              site {\n                updateConfig(\n                  authAutoLogin: $authAutoLogin,\n                  authEnforce2FA: $authEnforce2FA,\n                  authHideLocal: $authHideLocal,\n                  authLoginBgUrl: $authLoginBgUrl,\n                  authJwtAudience: $authJwtAudience,\n                  authJwtExpiration: $authJwtExpiration,\n                  authJwtRenewablePeriod: $authJwtRenewablePeriod,\n                  uploadMaxFileSize: $uploadMaxFileSize,\n                  uploadMaxFiles: $uploadMaxFiles,\n                  uploadScanSVG: $uploadScanSVG\n                  uploadForceDownload: $uploadForceDownload,\n                  securityOpenRedirect: $securityOpenRedirect,\n                  securityIframe: $securityIframe,\n                  securityReferrerPolicy: $securityReferrerPolicy,\n                  securityTrustProxy: $securityTrustProxy,\n                  securitySRI: $securitySRI,\n                  securityHSTS: $securityHSTS,\n                  securityHSTSDuration: $securityHSTSDuration,\n                  securityCSP: $securityCSP,\n                  securityCSPDirectives: $securityCSPDirectives\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            authAutoLogin: _.get(this.config, 'authAutoLogin', false),\n            authEnforce2FA: _.get(this.config, 'authEnforce2FA', false),\n            authHideLocal: _.get(this.config, 'authHideLocal', false),\n            authLoginBgUrl: _.get(this.config, 'authLoginBgUrl', ''),\n            authJwtAudience: _.get(this.config, 'authJwtAudience', ''),\n            authJwtExpiration: _.get(this.config, 'authJwtExpiration', ''),\n            authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),\n            uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),\n            uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),\n            uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),\n            uploadForceDownload: _.get(this.config, 'uploadForceDownload', false),\n            securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),\n            securityIframe: _.get(this.config, 'securityIframe', false),\n            securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),\n            securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),\n            securitySRI: _.get(this.config, 'securitySRI', false),\n            securityHSTS: _.get(this.config, 'securityHSTS', false),\n            securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),\n            securityCSP: _.get(this.config, 'securityCSP', false),\n            securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: 'Configuration saved successfully.',\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    },\n    browseLoginBg () {\n      this.$store.set('editor/editorKey', 'common')\n      this.activeModal = 'editorModalMedia'\n    }\n  },\n  mounted () {\n    this.$root.$on('editorInsert', opts => {\n      this.config.authLoginBgUrl = opts.path\n    })\n  },\n  beforeDestroy() {\n    this.$root.$off('editorInsert')\n  },\n  apollo: {\n    config: {\n      query: gql`\n        {\n          site {\n            config {\n              authAutoLogin\n              authEnforce2FA\n              authHideLocal\n              authLoginBgUrl\n              authJwtAudience\n              authJwtExpiration\n              authJwtRenewablePeriod\n              uploadMaxFileSize\n              uploadMaxFiles\n              uploadScanSVG\n              uploadForceDownload\n              securityOpenRedirect\n              securityIframe\n              securityReferrerPolicy\n              securityTrustProxy\n              securitySRI\n              securityHSTS\n              securityHSTSDuration\n              securityCSP\n              securityCSPDirectives\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.site.config),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-security-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-ssl.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-validation.svg', alt='SSL', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:ssl.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:ssl.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown(\n            v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0'\n            color='black'\n            dark\n            depressed\n            @click='renewCertificate'\n            large\n            :loading='loadingRenew'\n            )\n            v-icon(left) mdi-cached\n            span {{$t('admin:ssl.renewCertificate')}}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp\n                v-subheader {{ $t('admin:ssl.currentState') }}\n                v-list(two-line, dense)\n                  v-list-item\n                    v-list-item-avatar\n                      v-icon.indigo.white--text mdi-handshake\n                    v-list-item-content\n                      v-list-item-title {{ $t(`admin:ssl.provider`) }}\n                      v-list-item-subtitle {{ providerTitle }}\n                  template(v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0')\n                    v-list-item\n                      v-list-item-avatar\n                        v-icon.indigo.white--text mdi-application\n                      v-list-item-content\n                        v-list-item-title {{ $t(`admin:ssl.domain`) }}\n                        v-list-item-subtitle {{ info.sslDomain }}\n                    v-list-item\n                      v-list-item-avatar\n                        v-icon.indigo.white--text mdi-at\n                      v-list-item-content\n                        v-list-item-title {{ $t('admin:ssl.subscriberEmail') }}\n                        v-list-item-subtitle {{ info.sslSubscriberEmail }}\n                    v-list-item\n                      v-list-item-avatar\n                        v-icon.indigo.white--text mdi-calendar-remove-outline\n                      v-list-item-content\n                        v-list-item-title {{ $t('admin:ssl.expiration') }}\n                        v-list-item-subtitle {{ info.sslExpirationDate | moment('calendar') }}\n                    v-list-item\n                      v-list-item-avatar\n                        v-icon.indigo.white--text mdi-traffic-light\n                      v-list-item-content\n                        v-list-item-title {{ $t(`admin:ssl.status`) }}\n                        v-list-item-subtitle {{ info.sslStatus }}\n\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp.wait-p2s\n                v-subheader {{ $t('admin:ssl.ports') }}\n                v-list(two-line, dense)\n                  v-list-item\n                    v-list-item-avatar\n                      v-icon.blue.white--text mdi-lock-open-variant\n                    v-list-item-content\n                      v-list-item-title {{ $t(`admin:ssl.httpPort`) }}\n                      v-list-item-subtitle {{ info.httpPort }}\n                  template(v-if='info.httpsPort > 0')\n                    v-divider\n                    v-list-item\n                      v-list-item-avatar\n                        v-icon.green.white--text mdi-lock\n                      v-list-item-content\n                        v-list-item-title {{ $t(`admin:ssl.httpsPort`) }}\n                        v-list-item-subtitle {{ info.httpsPort }}\n                    v-divider\n                    v-list-item\n                      v-list-item-avatar\n                        v-icon.indigo.white--text mdi-sign-direction\n                      v-list-item-content\n                        v-list-item-title {{ $t(`admin:ssl.httpPortRedirect`) }}\n                        v-list-item-subtitle {{ info.httpRedirection }}\n                      v-list-item-action\n                        v-btn.red--text(\n                          v-if='info.httpRedirection'\n                          depressed\n                          :color='$vuetify.theme.dark ? `red darken-4` : `red lighten-5`'\n                          :class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'\n                          @click='toggleRedir'\n                          :loading='loadingRedir'\n                          )\n                          v-icon(left) mdi-power\n                          span {{$t('admin:ssl.httpPortRedirectTurnOff')}}\n                        v-btn.green--text(\n                          v-else\n                          depressed\n                          :color='$vuetify.theme.dark ? `green darken-4` : `green lighten-5`'\n                          :class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'\n                          @click='toggleRedir'\n                          :loading='loadingRedir'\n                          )\n                          v-icon(left) mdi-power\n                          span {{$t('admin:ssl.httpPortRedirectTurnOn')}}\n\n    v-dialog(\n      v-model='loadingRenew'\n      persistent\n      max-width='450'\n      )\n      v-card(color='black', dark)\n        v-card-text.pa-10.text-center\n          semipolar-spinner.animated.fadeIn(\n            :animation-duration='1500'\n            :size='65'\n            color='#FFF'\n            style='margin: 0 auto;'\n          )\n          .mt-5.body-1.white--text {{$t('admin:ssl.renewCertificateLoadingTitle')}}\n          .caption.mt-4 {{$t('admin:ssl.renewCertificateLoadingSubtitle')}}\n\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nimport { SemipolarSpinner } from 'epic-spinners'\n\nexport default {\n  components: {\n    SemipolarSpinner\n  },\n  data() {\n    return {\n      loadingRenew: false,\n      loadingRedir: false,\n      info: {\n        sslDomain: '',\n        sslProvider: '',\n        sslSubscriberEmail: '',\n        sslExpirationDate: false,\n        sslStatus: '',\n        httpPort: 0,\n        httpRedirection: false,\n        httpsPort: 0\n      }\n    }\n  },\n  computed: {\n    providerTitle () {\n      switch (this.info.sslProvider) {\n        case 'custom':\n          return this.$t('admin:ssl.providerCustomCertificate')\n        case 'letsencrypt':\n          return this.$t('admin:ssl.providerLetsEncrypt')\n        default:\n          return this.$t('admin:ssl.providerDisabled')\n      }\n    }\n  },\n  methods: {\n    async toggleRedir () {\n      this.loadingRedir = true\n      try {\n        this.info.httpRedirection = !this.info.httpRedirection\n        await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($enabled: Boolean!) {\n              system {\n                setHTTPSRedirection(enabled: $enabled) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            enabled: _.get(this.info, 'httpRedirection', false)\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-toggleRedirection')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:ssl.httpPortRedirectSaveSuccess'),\n          icon: 'check'\n        })\n      } catch (err) {\n        this.info.httpRedirection = !this.info.httpRedirection\n        this.$store.commit('pushGraphError', err)\n      }\n      this.loadingRedir = false\n    },\n    async renewCertificate () {\n      this.loadingRenew = true\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: gql`\n            mutation {\n              system {\n                renewHTTPSCertificate {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-renew')\n          }\n        })\n        const resp = _.get(respRaw, 'data.system.renewHTTPSCertificate.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('admin:ssl.renewCertificateSuccess'),\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.loadingRenew = false\n    }\n  },\n  apollo: {\n    info: {\n      query: gql`\n        {\n          system {\n            info {\n              httpPort\n              httpRedirection\n              httpsPort\n              sslDomain\n              sslExpirationDate\n              sslProvider\n              sslStatus\n              sslSubscriberEmail\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.system.info),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-stats.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, fill-height)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header-icon: v-icon(size='80', color='grey lighten-2') show_chart\n        .headline.primary--text Statistics\n        .subtitle-1.grey--text Useful information about your wiki\n        .pa-3\n          fingerprint-spinner(\n            :animation-duration='1500'\n            :size='128'\n            color='#e91e63'\n            )\n          .caption.pink--text.mt-3 Compiling latest data...\n</template>\n\n<script>\nimport { FingerprintSpinner } from 'epic-spinners'\n\nexport default {\n  components: {\n    FingerprintSpinner\n  },\n  data() {\n    return {}\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-storage.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('admin:storage.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('admin:storage.subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/storage', target='_blank')\n            v-icon mdi-help-circle\n          v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='primary', dark, dense)\n            .subtitle-1 {{$t('admin:storage.targets')}}\n          v-list(two-line, dense).py-0\n            template(v-for='(tgt, idx) in targets')\n              v-list-item(:key='tgt.key', @click='selectedTarget = tgt.key', :disabled='!tgt.isAvailable')\n                v-list-item-avatar(size='24')\n                  v-icon(color='grey', v-if='!tgt.isAvailable') mdi-minus-box-outline\n                  v-icon(color='primary', v-else-if='tgt.isEnabled', v-ripple, @click='tgt.key !== `local` && (tgt.isEnabled = false)') mdi-checkbox-marked-outline\n                  v-icon(color='grey', v-else, v-ripple, @click='tgt.isEnabled = true') mdi-checkbox-blank-outline\n                v-list-item-content\n                  v-list-item-title.body-2(:class='!tgt.isAvailable ? `grey--text` : (selectedTarget === tgt.key ? `primary--text` : ``)') {{ tgt.title }}\n                  v-list-item-subtitle: .caption(:class='!tgt.isAvailable ? `grey--text text--lighten-1` : (selectedTarget === tgt.key ? `blue--text ` : ``)') {{ tgt.description }}\n                v-list-item-avatar(v-if='selectedTarget === tgt.key', size='24')\n                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right\n              v-divider(v-if='idx < targets.length - 1')\n\n        v-card.mt-3.animated.fadeInUp.wait-p2s\n          v-toolbar(flat, :color='$vuetify.theme.dark ? `grey darken-3-l5` : `grey darken-3`', dark, dense)\n            .subtitle-1 {{$t('admin:storage.status')}}\n            v-spacer\n            looping-rhombuses-spinner(\n              :animation-duration='5000'\n              :rhombus-size='10'\n              color='#FFF'\n            )\n          v-list.py-0(two-line, dense)\n            template(v-for='(tgt, n) in status')\n              v-list-item(:key='tgt.key')\n                template(v-if='tgt.status === `pending`')\n                  v-list-item-avatar(color='purple')\n                    v-icon(color='white') mdi-clock-outline\n                  v-list-item-content\n                    v-list-item-title.body-2 {{tgt.title}}\n                    v-list-item-subtitle.purple--text.caption {{tgt.status}}\n                  v-list-item-action\n                    v-progress-circular(indeterminate, :size='20', :width='2', color='purple')\n                template(v-else-if='tgt.status === `operational`')\n                  v-list-item-avatar(color='green')\n                    v-icon(color='white') mdi-check-circle\n                  v-list-item-content\n                    v-list-item-title.body-2 {{tgt.title}}\n                    v-list-item-subtitle.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}\n                template(v-else)\n                  v-list-item-avatar(color='red')\n                    v-icon(color='white') mdi-close-circle-outline\n                  v-list-item-content\n                    v-list-item-title.body-2 {{tgt.title}}\n                    v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}\n                  v-list-item-action\n                    v-menu\n                      template(v-slot:activator='{ on }')\n                        v-btn(icon, v-on='on')\n                          v-icon(color='red') mdi-information\n                      v-card(width='450')\n                        v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}\n                        v-card-text {{tgt.message}}\n\n              v-divider(v-if='n < status.length - 1')\n            v-list-item(v-if='status.length < 1')\n              em {{$t('admin:storage.noTarget')}}\n\n      v-flex(xs12, lg9)\n        v-card.wiki-form.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{target.title}}\n            v-spacer\n            v-switch(\n              dark\n              color='blue lighten-5'\n              label='Active'\n              v-model='target.isEnabled'\n              hide-details\n              inset\n              )\n          v-card-info(color='blue')\n            div\n              div {{target.description}}\n              span.caption: a(:href='target.website') {{target.website}}\n            v-spacer\n            .admin-providerlogo\n              img(:src='target.logo', :alt='target.title')\n          v-card-text\n            v-form\n              i18next.body-2(path='admin:storage.targetState', tag='div', v-if='target.isEnabled')\n                v-chip(color='green', small, dark, label, place='state') {{$t('admin:storage.targetStateActive')}}\n              i18next.body-2(path='admin:storage.targetState', tag='div', v-else)\n                v-chip(color='red', small, dark, label, place='state') {{$t('admin:storage.targetStateInactive')}}\n              v-divider.mt-3\n              .overline.my-5 {{$t('admin:storage.targetConfig')}}\n              .body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}\n              template(v-else, v-for='cfg in target.config')\n                v-select(\n                  v-if='cfg.value.type === \"string\" && cfg.value.enum'\n                  outlined\n                  :items='cfg.value.enum'\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                )\n                v-switch.mb-3(\n                  v-else-if='cfg.value.type === \"boolean\"'\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  color='primary'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  inset\n                  )\n                v-textarea(\n                  v-else-if='cfg.value.type === \"string\" && cfg.value.multiline'\n                  outlined\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                  )\n                v-text-field(\n                  v-else\n                  outlined\n                  :key='cfg.key'\n                  :label='cfg.value.title'\n                  v-model='cfg.value.value'\n                  prepend-icon='mdi-cog-box'\n                  :hint='cfg.value.hint ? cfg.value.hint : \"\"'\n                  persistent-hint\n                  :class='cfg.value.hint ? \"mb-2\" : \"\"'\n                  )\n              v-divider.mt-3\n              .overline.my-5 {{$t('admin:storage.syncDirection')}}\n              .body-2.ml-3 {{$t('admin:storage.syncDirectionSubtitle')}}\n              .pr-3.pt-3\n                v-radio-group.ml-3.py-0(v-model='target.mode')\n                  v-radio(\n                    :label='$t(`admin:storage.syncDirBi`)'\n                    color='primary'\n                    value='sync'\n                    :disabled='target.supportedModes.indexOf(`sync`) < 0'\n                  )\n                  v-radio(\n                    :label='$t(`admin:storage.syncDirPush`)'\n                    color='primary'\n                    value='push'\n                    :disabled='target.supportedModes.indexOf(`push`) < 0'\n                  )\n                  v-radio(\n                    :label='$t(`admin:storage.syncDirPull`)'\n                    color='primary'\n                    value='pull'\n                    :disabled='target.supportedModes.indexOf(`pull`) < 0'\n                  )\n              .body-2.ml-3\n                strong {{$t('admin:storage.syncDirBi')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') {{$t('admin:storage.unsupported')}}]\n                .pb-3 {{$t('admin:storage.syncDirBiHint')}}\n                strong {{$t('admin:storage.syncDirPush')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') {{$t('admin:storage.unsupported')}}]\n                .pb-3 {{$t('admin:storage.syncDirPushHint')}}\n                strong {{$t('admin:storage.syncDirPull')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') {{$t('admin:storage.unsupported')}}]\n                .pb-3 {{$t('admin:storage.syncDirPullHint')}}\n\n              template(v-if='target.hasSchedule')\n                v-divider.mt-3\n                .overline.my-5 {{$t('admin:storage.syncSchedule')}}\n                .body-2.ml-3 {{$t('admin:storage.syncScheduleHint')}}\n                .pa-3\n                  duration-picker(v-model='target.syncInterval')\n                  i18next.caption.mt-3(path='admin:storage.syncScheduleCurrent', tag='div')\n                    strong(place='schedule') {{getDefaultSchedule(target.syncInterval)}}\n                  i18next.caption(path='admin:storage.syncScheduleDefault', tag='div')\n                    strong(place='schedule') {{getDefaultSchedule(target.syncIntervalDefault)}}\n\n              template(v-if='target.actions && target.actions.length > 0')\n                v-divider.mt-3\n                .overline.my-5 {{$t('admin:storage.actions')}}\n                v-alert(outlined, :value='!target.isEnabled', color='red', icon='mdi-alert')\n                  .body-2 {{$t('admin:storage.actionsInactiveWarn')}}\n                v-container.pt-0(grid-list-xl, fluid)\n                  v-layout(row, wrap, fill-height)\n                    v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')\n                      v-card.radius-7.grey(flat, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`', height='100%')\n                        v-card-text\n                          .subtitle-1(v-html='act.label')\n                          .body-2.mt-4(v-html='act.hint')\n                          v-btn.mx-0.mt-5(\n                            @click='executeAction(target.key, act.handler)'\n                            outlined\n                            :color='$vuetify.theme.dark ? `blue` : `primary`'\n                            :disabled='runningAction || !target.isEnabled'\n                            :loading='runningActionHandler === act.handler'\n                            ) {{$t('admin:storage.actionRun')}}\n\n</template>\n\n<script>\nimport _ from 'lodash'\nimport moment from 'moment'\nimport momentDurationFormatSetup from 'moment-duration-format'\n\nimport DurationPicker from '../common/duration-picker.vue'\nimport { LoopingRhombusesSpinner } from 'epic-spinners'\n\nimport statusQuery from 'gql/admin/storage/storage-query-status.gql'\nimport targetsQuery from 'gql/admin/storage/storage-query-targets.gql'\nimport targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'\nimport targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'\n\nmomentDurationFormatSetup(moment)\n\nexport default {\n  components: {\n    DurationPicker,\n    LoopingRhombusesSpinner\n  },\n  filters: {\n    startCase(val) { return _.startCase(val) }\n  },\n  data() {\n    return {\n      runningAction: false,\n      runningActionHandler: '',\n      selectedTarget: '',\n      target: {\n        supportedModes: []\n      },\n      targets: [],\n      status: []\n    }\n  },\n  computed: {\n    activeTargets() {\n      return _.filter(this.targets, 'isEnabled')\n    }\n  },\n  watch: {\n    selectedTarget(newValue, oldValue) {\n      this.target = _.find(this.targets, ['key', newValue]) || {}\n    },\n    targets(newValue, oldValue) {\n      this.selectedTarget = _.get(_.find(this.targets, ['isEnabled', true]), 'key', 'disk')\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.targets.refetch()\n      this.$store.commit('showNotification', {\n        message: 'List of storage targets has been refreshed.',\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async save() {\n      this.$store.commit(`loadingStart`, 'admin-storage-savetargets')\n      await this.$apollo.mutate({\n        mutation: targetsSaveMutation,\n        variables: {\n          targets: this.targets.map(tgt => _.pick(tgt, [\n            'isEnabled',\n            'key',\n            'config',\n            'mode',\n            'syncInterval'\n          ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))\n        }\n      })\n      this.$store.commit('showNotification', {\n        message: 'Storage configuration saved successfully.',\n        style: 'success',\n        icon: 'check'\n      })\n      this.$store.commit(`loadingStop`, 'admin-storage-savetargets')\n    },\n    getDefaultSchedule(val) {\n      if (!val) { return 'N/A' }\n      return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')\n    },\n    async executeAction(targetKey, handler) {\n      this.$store.commit(`loadingStart`, 'admin-storage-executeaction')\n      this.runningAction = true\n      this.runningActionHandler = handler\n      try {\n        await this.$apollo.mutate({\n          mutation: targetExecuteActionMutation,\n          variables: {\n            targetKey,\n            handler\n          }\n        })\n        this.$store.commit('showNotification', {\n          message: 'Action completed.',\n          style: 'success',\n          icon: 'check'\n        })\n      } catch (err) {\n        console.warn(err)\n      }\n      this.runningAction = false\n      this.runningActionHandler = ''\n      this.$store.commit(`loadingStop`, 'admin-storage-executeaction')\n    }\n  },\n  apollo: {\n    targets: {\n      query: targetsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.storage.targets).map(str => ({\n        ...str,\n        config: _.sortBy(str.config.map(cfg => ({\n          ...cfg,\n          value: JSON.parse(cfg.value)\n        })), [t => t.value.order])\n      })),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh')\n      }\n    },\n    status: {\n      query: statusQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.storage.status,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh')\n      },\n      pollInterval: 3000\n    }\n  }\n}\n</script>\n\n<style lang='scss' scoped>\n\n.targetlogo {\n  width: 250px;\n  height: 85px;\n  float:right;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n\n  img {\n    max-width: 100%;\n    max-height: 50px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-system.vue",
    "content": "<template lang='pug'>\n  v-container.admin-system(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-tune.svg', alt='System Info', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:system.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:system.subtitle') }}\n        v-layout.mt-3(row wrap)\n          v-flex(lg6 xs12)\n            v-card.animated.fadeInUp\n              v-btn.animated.fadeInLeft.wait-p2s.btn-animate-rotate(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, @click='refresh'): v-icon(color='grey') mdi-refresh\n              v-subheader Wiki.js\n              v-list(two-line, dense)\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue.white--text mdi-application-export\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.currentVersion') }}\n                    v-list-item-subtitle {{ info.currentVersion }}\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue.white--text mdi-inbox-arrow-up\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.latestVersion') }}\n                    v-list-item-subtitle {{ info.latestVersion }}\n                  v-list-item-action\n                    v-list-item-action-text {{ $t('admin:system.published') }} {{ info.latestVersionReleaseDate | moment('from') }}\n              v-card-actions(v-if='info.upgradeCapable && !isLatestVersion && info.platform === `docker`', :class='$vuetify.theme.dark ? `grey darken-3-d5` : `indigo lighten-5`')\n                .caption.indigo--text.pl-3(:class='$vuetify.theme.dark ? `text--lighten-4` : ``') Wiki.js can perform the upgrade to the latest version for you.\n                v-spacer\n                v-btn.px-3(\n                  color='indigo'\n                  dark\n                  @click='performUpgrade'\n                  )\n                  v-icon(left) mdi-upload\n                  span Perform Upgrade\n\n            v-card.mt-4.animated.fadeInUp.wait-p2s\n              v-subheader {{ $t('admin:system.hostInfo') }}\n              v-list(two-line, dense)\n                v-list-item\n                  v-list-item-avatar\n                    v-avatar.blue-grey(size='40')\n                      v-icon(color='white') {{platformLogo}}\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.os') }}\n                    v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue-grey.white--text mdi-desktop-classic\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.hostname') }}\n                    v-list-item-subtitle {{ info.hostname }}\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue-grey.white--text mdi-cpu-64-bit\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.cpuCores') }}\n                    v-list-item-subtitle {{ info.cpuCores }}\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue-grey.white--text mdi-memory\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.totalRAM') }}\n                    v-list-item-subtitle {{ info.ramTotal }}\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue-grey.white--text mdi-iframe-outline\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.workingDirectory') }}\n                    v-list-item-subtitle {{ info.workingDirectory }}\n                v-list-item\n                  v-list-item-avatar\n                    v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline\n                  v-list-item-content\n                    v-list-item-title {{ $t('admin:system.configFile') }}\n                    v-list-item-subtitle {{ info.configFile }}\n\n          v-flex(lg6 xs12)\n            v-card.pb-3.animated.fadeInUp.wait-p4s\n              v-subheader Node.js\n              v-list(dense)\n                v-list-item\n                  v-list-item-avatar\n                    v-avatar.light-green(size='40')\n                      v-icon(color='white') mdi-nodejs\n                  v-list-item-content\n                    v-list-item-title {{ info.nodeVersion }}\n\n              v-divider.mt-3\n              v-subheader {{ info.dbType }}\n              v-list(dense)\n                v-list-item\n                  v-list-item-avatar\n                    v-avatar.indigo.darken-1(size='40')\n                      v-icon(color='white') mdi-database\n                  v-list-item-content\n                    v-list-item-title(v-html='dbVersion')\n                    v-list-item-subtitle {{ info.dbHost }}\n\n                v-alert.mt-3.mx-4(:value='isDbLimited', color='deep-orange darken-2', icon='mdi-alert', dark) {{ $t('admin:system.dbPartialSupport') }}\n\n    v-dialog(\n      v-model='isUpgrading'\n      persistent\n      width='450'\n      )\n      v-card.blue.darken-5(dark)\n        v-card-text.text-center.pa-10\n          self-building-square-spinner(\n            :animation-duration='4000'\n            :size='40'\n            color='#FFF'\n            style='margin: 0 auto;'\n            )\n          .body-2.mt-5.blue--text.text--lighten-4 Your Wiki.js container is being upgraded...\n          .caption.blue--text.text--lighten-2 Please wait\n          v-progress-linear.mt-5(\n            color='blue lighten-2'\n            :value='upgradeProgress'\n            :buffer-value='upgradeProgress'\n            rounded\n            :stream='isUpgradingStarted'\n            query\n            :indeterminate='!isUpgradingStarted'\n          )\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport { SelfBuildingSquareSpinner } from 'epic-spinners'\n\nimport systemInfoQuery from 'gql/admin/system/system-query-info.gql'\nimport performUpgradeMutation from 'gql/admin/system/system-mutation-upgrade.gql'\n\nexport default {\n  components: {\n    SelfBuildingSquareSpinner\n  },\n  data () {\n    return {\n      isUpgrading: false,\n      isUpgradingStarted: false,\n      upgradeProgress: 0,\n      info: {}\n    }\n  },\n  computed: {\n    dbVersion () {\n      return _.get(this.info, 'dbVersion', '').replace(/(?:\\r\\n|\\r|\\n)/g, '<br />')\n    },\n    platformLogo () {\n      switch (this.info.platform) {\n        case 'docker':\n          return 'mdi-docker'\n        case 'darwin':\n          return 'mdi-apple'\n        case 'linux':\n          if (this.info.operatingSystem.indexOf('Ubuntu')) {\n            return 'mdi-ubuntu'\n          } else {\n            return 'mdi-linux'\n          }\n        case 'win32':\n          return 'mdi-microsoft-windows'\n        default:\n          return ''\n      }\n    },\n    isDbLimited () {\n      return this.info.dbType === 'MySQL' && this.dbVersion.indexOf('5.') === 0\n    },\n    isLatestVersion () {\n      return this.info.currentVersion === this.info.latestVersion\n    }\n  },\n  methods: {\n    async refresh () {\n      await this.$apollo.queries.info.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('admin:system.refreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    async performUpgrade () {\n      this.isUpgrading = true\n      this.isUpgradingStarted = false\n      this.upgradeProgress = 0\n      this.$store.commit(`loadingStart`, 'admin-system-upgrade')\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: performUpgradeMutation\n        })\n        const resp = _.get(respRaw, 'data.system.performUpgrade.responseResult', {})\n        if (resp.succeeded) {\n          this.isUpgradingStarted = true\n          let progressInterval = setInterval(() => {\n            this.upgradeProgress += 0.83\n          }, 500)\n          _.delay(() => {\n            clearInterval(progressInterval)\n            window.location.reload(true)\n          }, 60000)\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n        this.$store.commit(`loadingStop`, 'admin-system-upgrade')\n        this.isUpgrading = false\n      }\n    }\n  },\n  apollo: {\n    info: {\n      query: systemInfoQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.system.info,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.admin-system {\n  .v-list-item-title, .v-list-item__subtitle {\n    user-select: text;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-tags.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-tags.svg', alt='Tags', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('tags.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('tags.subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInDown(outlined, color='grey', @click='refresh', icon)\n            v-icon mdi-refresh\n        v-container.pa-0.mt-3(fluid, grid-list-lg)\n          v-layout(row)\n            v-flex(style='flex: 0 0 350px;')\n              v-card.animated.fadeInUp\n                v-toolbar(:color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', flat)\n                  v-text-field(\n                    v-model='filter'\n                    :label='$t(`admin:tags.filter`)'\n                    hide-details\n                    single-line\n                    solo\n                    flat\n                    dense\n                    color='teal'\n                    :background-color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-2`'\n                    prepend-inner-icon='mdi-magnify'\n                  )\n                v-divider\n                v-list.py-2(dense, nav)\n                  v-list-item(v-if='tags.length < 1')\n                    v-list-item-avatar(size='24'): v-icon(color='grey') mdi-compass-off\n                    v-list-item-content\n                      .caption.grey--text {{$t('tags.emptyList')}}\n                  v-list-item(\n                    v-for='tag of filteredTags'\n                    :key='tag.id'\n                    :class='(tag.id === current.id) ? \"teal\" : \"\"'\n                    @click='selectTag(tag)'\n                    )\n                    v-list-item-avatar(size='24', tile): v-icon(size='18', :color='tag.id === current.id ? `white` : `teal`') mdi-tag\n                    v-list-item-title(:class='tag.id === current.id ? `white--text` : ``') {{tag.tag}}\n            v-flex.animated.fadeInUp.wait-p2s\n              template(v-if='current.id')\n                v-card\n                  v-toolbar(dense, color='teal', flat, dark)\n                    .subtitle-1 {{$t('tags.edit')}}\n                    v-spacer\n                    v-btn.pl-4(\n                      color='white'\n                      dark\n                      outlined\n                      small\n                      :href='`/t/` + current.tag'\n                      )\n                      span.text-none {{$t('admin:tags.viewLinkedPages')}}\n                      v-icon(right) mdi-chevron-right\n                  v-card-text\n                    v-text-field(\n                      outlined\n                      :label='$t(\"tags.tag\")'\n                      prepend-icon='mdi-tag'\n                      v-model='current.tag'\n                      counter='255'\n                    )\n                    v-text-field(\n                      outlined\n                      :label='$t(\"tags.label\")'\n                      prepend-icon='mdi-format-title'\n                      v-model='current.title'\n                      hide-details\n                    )\n                  v-card-chin\n                    i18next.caption.pl-3(path='admin:tags.date', tag='div')\n                      strong(place='created') {{current.createdAt | moment('from')}}\n                      strong(place='updated') {{current.updatedAt | moment('from')}}\n                    v-spacer\n                    v-dialog(v-model='deleteTagDialog', max-width='500')\n                      template(v-slot:activator='{ on }')\n                        v-btn(color='red', outlined, v-on='on')\n                          v-icon(color='red') mdi-trash-can-outline\n                      v-card\n                        .dialog-header.is-red {{$t('admin:tags.deleteConfirm')}}\n                        v-card-text.pa-4\n                          i18next(tag='span', path='admin:tags.deleteConfirmText')\n                            strong(place='tag') {{ current.tag }}\n                        v-card-actions\n                          v-spacer\n                          v-btn(text, @click='deleteTagDialog = false') {{$t('common:actions.cancel')}}\n                          v-btn(color='red', dark, @click='deleteTag(current)') {{$t('common:actions.delete')}}\n                    v-btn.px-5.mr-2(color='success', depressed, dark, @click='saveTag(current)')\n                      v-icon(left) mdi-content-save\n                      span {{$t('common:actions.save')}}\n              v-card(v-else)\n                v-card-text.grey--text(v-if='tags.length > 0') {{$t('tags.noSelectionText')}}\n                v-card-text.grey--text(v-else) {{$t('tags.noItemsText')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nexport default {\n  data() {\n    return {\n      tags: [],\n      current: {},\n      filter: '',\n      deleteTagDialog: false\n    }\n  },\n  computed: {\n    filteredTags () {\n      if (this.filter.length > 0) {\n        return _.filter(this.tags, t => t.tag.indexOf(this.filter) >= 0 || t.title.indexOf(this.filter) >= 0)\n      } else {\n        return this.tags\n      }\n    }\n  },\n  methods: {\n    selectTag(tag) {\n      this.current = tag\n    },\n    async deleteTag(tag) {\n      this.$store.commit(`loadingStart`, 'admin-tags-delete')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($id: Int!) {\n              pages {\n                deleteTag (id: $id) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: tag.id\n          }\n        })\n        if (_.get(resp, 'data.pages.deleteTag.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('tags.deleteSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n          this.refresh()\n        } else {\n          throw new Error(_.get(resp, 'data.pages.deleteTag.responseResult.message', 'An unexpected error occurred.'))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.deleteTagDialog = false\n      this.$store.commit(`loadingStop`, 'admin-tags-delete')\n    },\n    async saveTag(tag) {\n      this.$store.commit(`loadingStart`, 'admin-tags-save')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($id: Int!, $tag: String!, $title: String!) {\n              pages {\n                updateTag (id: $id, tag: $tag, title: $title) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: tag.id,\n            tag: tag.tag,\n            title: tag.title\n          }\n        })\n        if (_.get(resp, 'data.pages.updateTag.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            message: this.$t('tags.saveSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n          this.current.updatedAt = new Date()\n        } else {\n          throw new Error(_.get(resp, 'data.pages.updateTag.responseResult.message', 'An unexpected error occurred.'))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-tags-save')\n    },\n    async refresh() {\n      await this.$apollo.queries.tags.refetch()\n      this.current = {}\n      this.$store.commit('showNotification', {\n        message: this.$t('tags.refreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    }\n  },\n  apollo: {\n    tags: {\n      query: gql`\n        {\n          pages {\n            tags {\n              id\n              tag\n              title\n              createdAt\n              updatedAt\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.pages.tags),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-tags-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss' scoped>\n\n.clickable {\n  cursor: pointer;\n\n  &:hover {\n    background-color: rgba(mc('blue', '500'), .25);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-theme.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-paint-palette.svg', alt='Theme', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('admin:theme.title')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:theme.subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInRight(color='success', depressed, @click='save', large, :loading='loading')\n            v-icon(left) mdi-check\n            span {{$t('common:actions.apply')}}\n        v-form.pt-3\n          v-layout(row wrap)\n            v-flex(lg6 xs12)\n              v-card.animated.fadeInUp\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{$t('admin:theme.title')}}\n                v-card-text\n                  v-select(\n                    :items='themes'\n                    outlined\n                    prepend-icon='mdi-palette'\n                    v-model='config.theme'\n                    :label='$t(`admin:theme.siteTheme`)'\n                    persistent-hint\n                    :hint='$t(`admin:theme.siteThemeHint`)'\n                    )\n                    template(slot='item', slot-scope='data')\n                      v-list-item-avatar\n                        v-icon.blue--text(dark) mdi-image-filter-frames\n                      v-list-item-content\n                        v-list-item-title(v-html='data.item.text')\n                        v-list-item-sub-title(v-html='data.item.author')\n                  v-select.mt-3(\n                    :items='iconsets'\n                    outlined\n                    prepend-icon='mdi-paw'\n                    v-model='config.iconset'\n                    :label='$t(`admin:theme.iconset`)'\n                    persistent-hint\n                    :hint='$t(`admin:theme.iconsetHint`)'\n                    )\n                  v-divider.mt-3\n                  v-switch(\n                    inset\n                    v-model='darkMode'\n                    :label='$t(`admin:theme.darkMode`)'\n                    color='primary'\n                    persistent-hint\n                    :hint='$t(`admin:theme.darkModeHint`)'\n                    )\n\n              v-card.mt-3.animated.fadeInUp.wait-p1s\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{$t(`admin:theme.options`)}}\n                v-card-text\n                  v-select(\n                    :items='tocPositions'\n                    outlined\n                    prepend-icon='mdi-border-vertical'\n                    v-model='config.tocPosition'\n                    label='Table of Contents Position'\n                    persistent-hint\n                    hint='Select whether the table of contents is shown on the left, right or not at all.'\n                    )\n            v-flex(lg6 xs12)\n              //- v-card.animated.fadeInUp.wait-p2s\n              //-   v-toolbar(color='teal', dark, dense, flat)\n              //-     v-toolbar-title.subtitle-1 {{$t('admin:theme.downloadThemes')}}\n              //-     v-spacer\n              //-     v-chip(label, color='white', small).teal--text coming soon\n              //-   v-data-table(\n              //-     :headers='headers',\n              //-     :items='themes',\n              //-     hide-default-footer,\n              //-     item-key='value',\n              //-     :items-per-page='1000'\n              //-   )\n              //-     template(v-slot:item='thm')\n              //-       td\n              //-         strong {{thm.item.text}}\n              //-       td\n              //-         span {{ thm.item.author }}\n              //-       td.text-xs-center\n              //-         v-progress-circular(v-if='thm.item.isDownloading', indeterminate, color='blue', size='20', :width='2')\n              //-         v-btn(v-else-if='thm.item.isInstalled && thm.item.installDate < thm.item.updatedAt', icon)\n              //-           v-icon.blue--text mdi-cached\n              //-         v-btn(v-else-if='thm.item.isInstalled', icon)\n              //-           v-icon.green--text mdi-check-bold\n              //-         v-btn(v-else, icon)\n              //-           v-icon.grey--text mdi-cloud-download\n\n              v-card.animated.fadeInUp.wait-p2s\n                v-toolbar(color='primary', dark, dense, flat)\n                  v-toolbar-title.subtitle-1 {{$t(`admin:theme.codeInjection`)}}\n                v-card-text\n                  v-textarea.is-monospaced(\n                    v-model='config.injectCSS'\n                    :label='$t(`admin:theme.cssOverride`)'\n                    outlined\n                    color='primary'\n                    persistent-hint\n                    :hint='$t(`admin:theme.cssOverrideHint`)'\n                    auto-grow\n                    )\n                  i18next.caption.pl-2.ml-1(path='admin:theme.cssOverrideWarning', tag='div')\n                    strong.red--text(place='caution') {{$t('admin:theme.cssOverrideWarningCaution')}}\n                    code(place='cssClass') .contents\n                  v-textarea.is-monospaced.mt-3(\n                    v-model='config.injectHead'\n                    :label='$t(`admin:theme.headHtmlInjection`)'\n                    outlined\n                    color='primary'\n                    persistent-hint\n                    :hint='$t(`admin:theme.headHtmlInjectionHint`)'\n                    auto-grow\n                    )\n                  v-textarea.is-monospaced.mt-2(\n                    v-model='config.injectBody'\n                    :label='$t(`admin:theme.bodyHtmlInjection`)'\n                    outlined\n                    color='primary'\n                    persistent-hint\n                    :hint='$t(`admin:theme.bodyHtmlInjectionHint`)'\n                    auto-grow\n                    )\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync } from 'vuex-pathify'\n\nimport themeConfigQuery from 'gql/admin/theme/theme-query-config.gql'\nimport themeSaveMutation from 'gql/admin/theme/theme-mutation-save.gql'\n\nexport default {\n  data() {\n    return {\n      loading: false,\n      themes: [\n        { text: 'Default', author: 'requarks.io', value: 'default', isInstalled: true, installDate: '', updatedAt: '' }\n      ],\n      iconsets: [\n        { text: 'Material Design Icons (default)', value: 'mdi' },\n        { text: 'Font Awesome 5', value: 'fa' },\n        { text: 'Font Awesome 4', value: 'fa4' }\n      ],\n      config: {\n        theme: 'default',\n        darkMode: false,\n        iconset: '',\n        tocPosition: 'left',\n        injectCSS: '',\n        injectHead: '',\n        injectBody: ''\n      },\n      darkModeInitial: false\n    }\n  },\n  computed: {\n    darkMode: sync('site/dark'),\n    headers() {\n      return [\n        {\n          text: this.$t('admin:theme.downloadName'),\n          align: 'left',\n          value: 'text'\n        },\n        {\n          text: this.$t('admin:theme.downloadAuthor'),\n          align: 'left',\n          value: 'author'\n        },\n        {\n          text: this.$t('admin:theme.downloadDownload'),\n          align: 'center',\n          value: 'value',\n          sortable: false,\n          width: 100\n        }\n      ]\n    },\n    tocPositions () {\n      return [\n        { text: 'Left (default)', value: 'left' },\n        { text: 'Right', value: 'right' },\n        { text: 'Hidden', value: 'off' }\n      ]\n    }\n  },\n  watch: {\n    'darkMode' (newValue, oldValue) {\n      this.$vuetify.theme.dark = newValue\n    }\n  },\n  mounted() {\n    this.darkModeInitial = this.darkMode\n  },\n  beforeDestroy() {\n    this.darkMode = this.darkModeInitial\n    this.$vuetify.theme.dark = this.darkModeInitial\n  },\n  methods: {\n    async save () {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-theme-save')\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: themeSaveMutation,\n          variables: {\n            theme: this.config.theme,\n            iconset: this.config.iconset,\n            darkMode: this.darkMode,\n            tocPosition: this.config.tocPosition,\n            injectCSS: this.config.injectCSS,\n            injectHead: this.config.injectHead,\n            injectBody: this.config.injectBody\n          }\n        })\n        const resp = _.get(respRaw, 'data.theming.setConfig.responseResult', {})\n        if (resp.succeeded) {\n          this.darkModeInitial = this.darkMode\n          this.$store.commit('showNotification', {\n            message: 'Theme settings updated successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.$store.commit(`loadingStop`, 'admin-theme-save')\n      this.loading = false\n    }\n  },\n  apollo: {\n    config: {\n      query: themeConfigQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.theming.config,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-theme-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.v-textarea.is-monospaced textarea {\n  font-family: 'Roboto Mono', 'Courier New', Courier, monospace;\n  font-size: 13px;\n  font-weight: 600;\n  line-height: 1.4;\n}\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-users-create.vue",
    "content": "<template lang=\"pug\">\n  v-dialog(v-model='isShown', max-width='650', persistent)\n    v-card\n      .dialog-header.is-short\n        v-icon.mr-3(color='white') mdi-plus\n        span New User\n        v-spacer\n        v-btn.mx-0(color='white', outlined, disabled, dark)\n          v-icon(left) mdi-database-import\n          span Bulk Import\n      v-card-text.pt-5\n        v-select(\n          :items='providers'\n          item-text='displayName'\n          item-value='key'\n          outlined\n          prepend-icon='mdi-domain'\n          v-model='provider'\n          label='Provider'\n          )\n        v-text-field(\n          outlined\n          prepend-icon='mdi-at'\n          v-model='email'\n          label='Email Address'\n          key='newUserEmail'\n          persistent-hint\n          ref='emailInput'\n          )\n        v-text-field(\n          v-if='provider === `local`'\n          outlined\n          prepend-icon='mdi-lock-outline'\n          append-icon='mdi-dice-5'\n          v-model='password'\n          :label='mustChangePwd ? `Temporary Password` : `Password`'\n          counter='255'\n          @click:append='generatePwd'\n          key='newUserPassword'\n          persistent-hint\n          )\n        v-text-field(\n          outlined\n          prepend-icon='mdi-account-outline'\n          v-model='name'\n          label='Name'\n          :hint='provider === `local` ? `Can be changed by the user.` : `May be overwritten by the provider during login.`'\n          key='newUserName'\n          persistent-hint\n          )\n        v-select.mt-2(\n          :items='groups'\n          item-text='name'\n          item-value='id'\n          item-disabled='isSystem'\n          outlined\n          prepend-icon='mdi-account-group'\n          v-model='group'\n          label='Assign to Group(s)...'\n          hint='Note that you cannot assign users to the Administrators or Guests groups from this dialog.'\n          persistent-hint\n          clearable\n          multiple\n          )\n        v-divider\n        v-checkbox(\n          color='primary'\n          label='Require password change on first login'\n          v-if='provider === `local`'\n          v-model='mustChangePwd'\n          hide-details\n        )\n        //- v-checkbox(\n        //-   color='primary'\n        //-   label='Send a welcome email'\n        //-   hide-details\n        //-   v-model='sendWelcomeEmail'\n        //-   disabled\n        //- )\n      v-card-chin\n        v-spacer\n        v-btn(text, @click='isShown = false') Cancel\n        v-btn.px-3(depressed, color='primary', @click='newUser(false)')\n          v-icon(left) mdi-chevron-right\n          span Create\n        v-btn.px-3(depressed, color='primary', @click='newUser(true)')\n          v-icon(left) mdi-chevron-double-right\n          span Create and Close\n</template>\n\n<script>\nimport _ from 'lodash'\nimport validate from 'validate.js'\nimport gql from 'graphql-tag'\n\nimport createUserMutation from 'gql/admin/users/users-mutation-create.gql'\nimport groupsQuery from 'gql/admin/users/users-query-groups.gql'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      providers: [],\n      provider: 'local',\n      email: '',\n      password: '',\n      name: '',\n      groups: [],\n      group: [],\n      mustChangePwd: false,\n      sendWelcomeEmail: false\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    }\n  },\n  watch: {\n    value(newValue, oldValue) {\n      if (newValue) {\n        this.$nextTick(() => {\n          this.$refs.emailInput.focus()\n        })\n      }\n    }\n  },\n  methods: {\n    async newUser(close = false) {\n      let rules = {\n        email: {\n          presence: {\n            allowEmpty: false\n          },\n          email: true\n        },\n        name: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255\n          }\n        }\n      }\n      if (this.provider === `local`) {\n        rules.password = {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6,\n            maximum: 255\n          }\n        }\n      }\n      const validationResults = validate({\n        email: this.email,\n        password: this.password,\n        name: this.name\n      }, rules, { format: 'flat' })\n\n      if (validationResults) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: validationResults[0],\n          icon: 'alert'\n        })\n        return\n      }\n\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: createUserMutation,\n          variables: {\n            providerKey: this.provider,\n            email: this.email,\n            passwordRaw: this.password,\n            name: this.name,\n            groups: this.group,\n            mustChangePassword: this.mustChangePwd,\n            sendWelcomeEmail: this.sendWelcomeEmail\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-create')\n          }\n        })\n        if (_.get(resp, 'data.users.create.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: 'New user created successfully.',\n            icon: 'check'\n          })\n\n          this.email = ''\n          this.password = ''\n          this.name = ''\n\n          if (close) {\n            this.isShown = false\n            this.$emit('refresh')\n          } else {\n            this.$refs.emailInput.focus()\n          }\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: _.get(resp, 'data.users.create.responseResult.message', 'An unexpected error occurred.'),\n            icon: 'alert'\n          })\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    },\n    generatePwd() {\n      const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'\n      this.password = _.sampleSize(pwdChars, 12).join('')\n    }\n  },\n  apollo: {\n    providers: {\n      query: gql`\n        query {\n          authentication {\n            activeStrategies {\n              key\n              displayName\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.authentication.activeStrategies,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')\n      }\n    },\n    groups: {\n      query: groupsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/admin/admin-users-edit.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-male-user.svg', :alt='$t(`admin:users.edit`)', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2.animated.fadeInLeft {{$t('admin:users.edit')}}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}}\n          v-spacer\n          i18next.pr-4.caption.grey--text.animated.fadeInDown(path='admin:users.id', tag='div')\n            strong(place='id') {{user.id}}\n          template(v-if='user.isActive')\n            status-indicator.mr-3(positive, pulse)\n            .caption.green--text {{$t('admin:users.active')}}\n          template(v-else)\n            status-indicator.mr-3(negative, pulse)\n            .caption.red--text {{$t('admin:users.inactive')}}\n          template(v-if='user.isVerified')\n            status-indicator.mr-3.ml-4(active, pulse)\n            .caption.blue--text {{$t('admin:users.verified')}}\n          template(v-else)\n            status-indicator.mr-3.ml-4(intermediary, pulse)\n            .caption.deep-orange--text {{$t('admin:users.unverified')}}\n          v-spacer\n          v-btn.ml-3.animated.fadeInDown.wait-p3s(color='grey', icon, outlined, to='/users')\n            v-icon mdi-arrow-left\n          v-menu(offset-y, origin='top right')\n            template(v-slot:activator='{ on }')\n              v-btn.ml-3.animated.fadeInDown.wait-p2s(color='black', v-on='on', depressed, dark)\n                span Actions\n                v-icon(right) mdi-chevron-down\n            v-list(dense, nav)\n              v-list-item(v-if='!user.isActive', @click='activateUser')\n                v-list-item-icon\n                  v-icon(color='purple') mdi-account-key\n                v-list-item-title Activate\n              v-list-item(v-else, @click='deactivateUser', :disabled='user.id == currentUserId || user.isSystem')\n                v-list-item-icon\n                  v-icon(color='purple') mdi-account-cancel\n                v-list-item-title Deactivate\n              v-list-item(@click='verifyUser', :disabled='user.isVerified')\n                v-list-item-icon\n                  v-icon(color='blue') mdi-account-check\n                v-list-item-title Set as Verified\n              v-list-item(@click='deleteUserConfirm', :disabled='user.id == currentUserId || user.isSystem')\n                v-list-item-icon\n                  v-icon(color='red') mdi-trash-can-outline\n                v-list-item-title Delete\n          v-btn.ml-3.animated.fadeInDown(color='primary', large, depressed, @click='updateUser')\n            v-icon(left) mdi-check\n            span {{$t('admin:users.updateUser')}}\n      v-flex(xs6)\n        v-card.animated.fadeInUp\n          v-toolbar(color='primary', dense, dark, flat)\n            v-icon.mr-2 mdi-information-variant\n            span {{$t('admin:users.basicInfo')}}\n          v-list.py-0(two-line, dense)\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-email-variant\n              v-list-item-content\n                v-list-item-title {{$t('admin:users.email')}}\n                v-list-item-subtitle {{ user.email }}\n              v-list-item-action(v-if='!user.isSystem && user.providerKey === `local`')\n                v-menu(\n                  v-model='editPop.email'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptEmail`)')\n                      v-icon mdi-pencil\n                  v-card\n                    v-text-field(\n                      ref='iptEmail'\n                      v-model='user.email'\n                      :label='$t(`admin:users.email`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.email = false'\n                      @keydown.enter='editPop.email = false'\n                      @keydown.esc='editPop.email = false'\n                    )\n\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-account\n              v-list-item-content\n                v-list-item-title {{$t('admin:users.displayName')}}\n                v-list-item-subtitle {{ user.name }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.name'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptDisplayName`)')\n                      v-icon mdi-pencil\n                  v-card\n                    v-text-field(\n                      ref='iptDisplayName'\n                      v-model='user.name'\n                      :label='$t(`admin:users.displayName`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.name = false'\n                      @keydown.enter='editPop.name = false'\n                      @keydown.esc='editPop.name = false'\n                    )\n\n        v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem')\n          v-toolbar(color='primary', dense, dark, flat)\n            v-icon.mr-2 mdi-lock-outline\n            span {{$t('admin:users.authentication')}}\n          v-list.py-0(two-line, dense)\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-domain\n              v-list-item-content\n                v-list-item-title {{$t('admin:users.authProvider')}}\n                v-list-item-subtitle {{ user.providerName }} #[em.caption ({{ user.providerKey }})]\n            template(v-if='user.providerKey === `local`')\n              v-divider\n              v-list-item\n                v-list-item-avatar(size='32')\n                  v-icon mdi-form-textbox-password\n                v-list-item-content\n                  v-list-item-title {{$t('admin:users.password')}}\n                  v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;\n                v-list-item-action\n                  v-menu(\n                    v-model='editPop.newPassword'\n                    :close-on-content-click='false'\n                    min-width='350'\n                    left\n                    )\n                    template(v-slot:activator='{ on: menu }')\n                      v-tooltip(top)\n                        template(v-slot:activator='{ on: tooltip }')\n                          v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')\n                            v-icon mdi-pencil\n                        span {{$t('admin:users.changePassword')}}\n                    v-card\n                      v-text-field(\n                        ref='iptNewPassword'\n                        v-model='newPassword'\n                        :label='$t(`admin:users.newPassword`)'\n                        solo\n                        hide-details\n                        append-icon='mdi-check'\n                        type='password'\n                        @click:append='editPop.newPassword = false'\n                        @keydown.enter='editPop.newPassword = false'\n                        @keydown.esc='editPop.newPassword = false'\n                      )\n                v-list-item-action\n                  v-tooltip(top)\n                    template(v-slot:activator='{ on }')\n                      v-btn(icon, color='grey', x-small, v-on='on', disabled)\n                        v-icon mdi-email\n                    span Send Password Reset Email\n            template(v-if='user.providerIs2FACapable')\n              v-divider\n              v-list-item\n                v-list-item-avatar(size='32')\n                  v-icon mdi-two-factor-authentication\n                v-list-item-content\n                  v-list-item-title {{$t('admin:users.tfa')}}\n                  v-list-item-subtitle.green--text(v-if='user.tfaIsActive') Active\n                  v-list-item-subtitle.red--text(v-else) Inactive\n                v-list-item-action\n                  v-tooltip(top)\n                    template(v-slot:activator='{ on }')\n                      v-btn(icon, color='grey', x-small, v-on='on', @click='toggle2FA')\n                        v-icon mdi-power\n                    span {{$t('admin:users.toggle2FA')}}\n            template(v-if='user.providerId')\n              v-divider\n              v-list-item\n                v-list-item-avatar(size='32')\n                  v-icon mdi-music-accidental-sharp\n                v-list-item-content\n                  v-list-item-title {{$t('admin:users.authProviderId')}}\n                  v-list-item-subtitle {{ user.providerId }}\n        v-card.mt-3.animated.fadeInUp.wait-p4s\n          v-toolbar(color='primary', dense, dark, flat)\n            v-icon.mr-2 mdi-account-group\n            span {{$t('admin:users.groups')}}\n          v-list(dense)\n            template(v-for='(group, idx) in user.groups')\n              v-list-item(:key='`group-` + group.id')\n                v-list-item-avatar(size='32')\n                  v-icon mdi-account-group-outline\n                v-list-item-content\n                  v-list-item-title {{group.name}}\n                v-list-item-action(v-if='!user.isSystem')\n                  v-btn(icon, color='red', x-small, @click='unassignGroup(group.id)')\n                    v-icon mdi-close\n              v-divider(v-if='idx < user.groups.length - 1')\n          v-alert.mx-3(v-if='user.groups.length < 1', outlined, color='grey darken-1', icon='mdi-alert')\n            .caption {{$t('admin:users.noGroupAssigned')}}\n          v-card-chin(v-if='!user.isSystem')\n            v-spacer\n            v-select(\n              ref='iptAssignGroup'\n              :items='groups'\n              v-model='newGroup'\n              :label='$t(`admin:users.selectGroup`)'\n              item-value='id'\n              item-text='name'\n              item-disabled='isSystem'\n              solo\n              flat\n              hide-details\n              @keydown.esc='editPop.assignGroup = false'\n              style='max-width: 300px;'\n              dense\n            )\n            v-btn.ml-2.px-4(depressed, color='primary', @click='assignGroup', :disabled='newGroup === 0')\n              v-icon(left) mdi-clipboard-account-outline\n              span {{$t('admin:users.groupAssign')}}\n          v-system-bar(window, :color='$vuetify.theme.dark ? `grey darken-4-l3` : `grey lighten-3`')\n            v-spacer\n            .caption {{$t('admin:users.groupAssignNotice')}}\n\n      v-flex(xs6)\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, dark, flat)\n            v-icon.mr-2 mdi-account-badge-outline\n            span {{$t('admin:users.extendedMetadata')}}\n          v-list.py-0(two-line, dense)\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-map-marker\n              v-list-item-content\n                v-list-item-title {{$t('admin:users.location')}}\n                v-list-item-subtitle {{ user.location }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.location'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptLocation`)')\n                      v-icon mdi-pencil\n                  v-card\n                    v-text-field(\n                      ref='iptLocation'\n                      v-model='user.location'\n                      :label='$t(`admin:users.location`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.location = false'\n                      @keydown.enter='editPop.location = false'\n                      @keydown.esc='editPop.location = false'\n                    )\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-briefcase\n              v-list-item-content\n                v-list-item-title {{$t('admin:users.jobTitle')}}\n                v-list-item-subtitle {{ user.jobTitle }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.jobTitle'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptJobTitle`)')\n                      v-icon mdi-pencil\n                  v-card\n                    v-text-field(\n                      ref='iptJobTitle'\n                      v-model='user.jobTitle'\n                      :label='$t(`admin:users.jobTitle`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.jobTitle = false'\n                      @keydown.enter='editPop.jobTitle = false'\n                      @keydown.esc='editPop.jobTitle = false'\n                    )\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-map-clock-outline\n              v-list-item-content\n                v-list-item-title {{$t('admin:users.timezone')}}\n                v-list-item-subtitle {{ user.timezone }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.timezone'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptTimezone`)')\n                      v-icon mdi-pencil\n                  v-card\n                    v-select(\n                      ref='iptTimezone'\n                      :items='timezones'\n                      v-model='user.timezone'\n                      :label='$t(`admin:users.timezone`)'\n                      solo\n                      dense\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.timezone = false'\n                      @keydown.enter='editPop.timezone = false'\n                      @keydown.esc='editPop.timezone = false'\n                    )\n\n        v-card.mt-3.animated.fadeInUp.wait-p4s\n          v-toolbar(color='teal', dark, dense, flat)\n            v-toolbar-title\n              .subtitle-1 {{$t('profile:activity.title')}}\n          v-card-text.grey--text.text--darken-2\n            .caption.grey--text {{$t('profile:activity.joinedOn')}}\n            .body-2: strong {{ user.createdAt | moment('LLLL') }}\n            .caption.grey--text.mt-3 {{$t('profile:activity.lastUpdatedOn')}}\n            .body-2: strong {{ user.updatedAt | moment('LLLL') }}\n            .caption.grey--text.mt-3 {{$t('profile:activity.lastLoginOn')}}\n            .body-2: strong {{ user.lastLoginAt | moment('LLLL') }}\n\n        //- v-card.mt-3.animated.fadeInUp.wait-p6s\n        //-   v-toolbar(color='teal', dense, dark, flat)\n        //-     v-icon.mr-2 mdi-file-document-box-multiple-outline\n        //-     span Content\n        //-   v-card-text\n        //-     em.caption.grey--text Coming soon\n\n    v-dialog(v-model='deleteUserDialog', max-width='500')\n      v-card\n        .dialog-header.is-red {{$t('admin:users.deleteConfirmTitle')}}\n        v-card-text.pt-5\n          i18next(path='admin:users.deleteConfirmText', tag='span')\n            strong(place='username') {{ user.email }}\n          .mt-3 {{$t('admin:users.deleteConfirmReplaceWarn')}}\n          v-divider.my-3\n          .d-flex.align-center.mt-3\n            v-btn.text-none(color='primary', depressed, @click='deleteSearchUserDialog = true')\n              v-icon(left) mdi-clipboard-account\n              | Select User...\n            .caption.pl-3\n              strong ID {{deleteReplaceUser.id}}\n              .caption {{deleteReplaceUser.name}}\n              em {{deleteReplaceUser.email}}\n        v-card-chin\n          v-spacer\n          v-btn(text, @click='deleteUserDialog = false') {{$t('common:actions.cancel')}}\n          v-btn(color='red', dark, @click='deleteUser') {{$t('common:actions.delete')}}\n\n        user-search(v-model='deleteSearchUserDialog', @select='assignDeleteUser')\n\n</template>\n<script>\nimport _ from 'lodash'\nimport { get } from 'vuex-pathify'\nimport gql from 'graphql-tag'\nimport { StatusIndicator } from 'vue-status-indicator'\n\nimport UserSearch from '../common/user-search.vue'\n\nimport groupsQuery from 'gql/admin/users/users-query-groups.gql'\n\nexport default {\n  i18nOptions: {\n    namespaces: ['admin', 'profile']\n  },\n  components: {\n    StatusIndicator,\n    UserSearch\n  },\n  data () {\n    return {\n      deleteUserDialog: false,\n      deleteSearchUserDialog: false,\n      deleteReplaceUser: {\n        id: 1,\n        name: '',\n        email: ''\n      },\n      editPop: {\n        email: false,\n        name: false,\n        pwd: false,\n        location: false,\n        jobTitle: false,\n        timezone: false,\n        newPassword: false,\n        assignGroup: false\n      },\n      newGroup: 0,\n      newPassword: '',\n      user: {\n        email: '',\n        name: '',\n        location: '',\n        jobTitle: '',\n        timezone: '',\n        groups: [],\n        isActive: false,\n        isVerified: false\n      },\n      timezones: [\n        { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },\n        { text: '(GMT-11:00) Pago Pago', value: 'Pacific/Pago_Pago' },\n        { text: '(GMT-10:00) Hawaii Time', value: 'Pacific/Honolulu' },\n        { text: '(GMT-10:00) Rarotonga', value: 'Pacific/Rarotonga' },\n        { text: '(GMT-10:00) Tahiti', value: 'Pacific/Tahiti' },\n        { text: '(GMT-09:30) Marquesas', value: 'Pacific/Marquesas' },\n        { text: '(GMT-09:00) Alaska Time', value: 'America/Anchorage' },\n        { text: '(GMT-09:00) Gambier', value: 'Pacific/Gambier' },\n        { text: '(GMT-08:00) Pacific Time', value: 'America/Los_Angeles' },\n        { text: '(GMT-08:00) Pacific Time - Tijuana', value: 'America/Tijuana' },\n        { text: '(GMT-08:00) Pacific Time - Vancouver', value: 'America/Vancouver' },\n        { text: '(GMT-08:00) Pacific Time - Whitehorse', value: 'America/Whitehorse' },\n        { text: '(GMT-08:00) Pitcairn', value: 'Pacific/Pitcairn' },\n        { text: '(GMT-07:00) Mountain Time', value: 'America/Denver' },\n        { text: '(GMT-07:00) Mountain Time - Arizona', value: 'America/Phoenix' },\n        { text: '(GMT-07:00) Mountain Time - Chihuahua, Mazatlan', value: 'America/Mazatlan' },\n        { text: '(GMT-07:00) Mountain Time - Dawson Creek', value: 'America/Dawson_Creek' },\n        { text: '(GMT-07:00) Mountain Time - Edmonton', value: 'America/Edmonton' },\n        { text: '(GMT-07:00) Mountain Time - Hermosillo', value: 'America/Hermosillo' },\n        { text: '(GMT-07:00) Mountain Time - Yellowknife', value: 'America/Yellowknife' },\n        { text: '(GMT-06:00) Belize', value: 'America/Belize' },\n        { text: '(GMT-06:00) Central Time', value: 'America/Chicago' },\n        { text: '(GMT-06:00) Central Time - Mexico City', value: 'America/Mexico_City' },\n        { text: '(GMT-06:00) Central Time - Regina', value: 'America/Regina' },\n        { text: '(GMT-06:00) Central Time - Tegucigalpa', value: 'America/Tegucigalpa' },\n        { text: '(GMT-06:00) Central Time - Winnipeg', value: 'America/Winnipeg' },\n        { text: '(GMT-06:00) Costa Rica', value: 'America/Costa_Rica' },\n        { text: '(GMT-06:00) El Salvador', value: 'America/El_Salvador' },\n        { text: '(GMT-06:00) Galapagos', value: 'Pacific/Galapagos' },\n        { text: '(GMT-06:00) Guatemala', value: 'America/Guatemala' },\n        { text: '(GMT-06:00) Managua', value: 'America/Managua' },\n        { text: '(GMT-05:00) America Cancun', value: 'America/Cancun' },\n        { text: '(GMT-05:00) Bogota', value: 'America/Bogota' },\n        { text: '(GMT-05:00) Easter Island', value: 'Pacific/Easter' },\n        { text: '(GMT-05:00) Eastern Time', value: 'America/New_York' },\n        { text: '(GMT-05:00) Eastern Time - Iqaluit', value: 'America/Iqaluit' },\n        { text: '(GMT-05:00) Eastern Time - Toronto', value: 'America/Toronto' },\n        { text: '(GMT-05:00) Guayaquil', value: 'America/Guayaquil' },\n        { text: '(GMT-05:00) Havana', value: 'America/Havana' },\n        { text: '(GMT-05:00) Jamaica', value: 'America/Jamaica' },\n        { text: '(GMT-05:00) Lima', value: 'America/Lima' },\n        { text: '(GMT-05:00) Nassau', value: 'America/Nassau' },\n        { text: '(GMT-05:00) Panama', value: 'America/Panama' },\n        { text: '(GMT-05:00) Port-au-Prince', value: 'America/Port-au-Prince' },\n        { text: '(GMT-05:00) Rio Branco', value: 'America/Rio_Branco' },\n        { text: '(GMT-04:00) Atlantic Time - Halifax', value: 'America/Halifax' },\n        { text: '(GMT-04:00) Barbados', value: 'America/Barbados' },\n        { text: '(GMT-04:00) Bermuda', value: 'Atlantic/Bermuda' },\n        { text: '(GMT-04:00) Boa Vista', value: 'America/Boa_Vista' },\n        { text: '(GMT-04:00) Caracas', value: 'America/Caracas' },\n        { text: '(GMT-04:00) Curacao', value: 'America/Curacao' },\n        { text: '(GMT-04:00) Grand Turk', value: 'America/Grand_Turk' },\n        { text: '(GMT-04:00) Guyana', value: 'America/Guyana' },\n        { text: '(GMT-04:00) La Paz', value: 'America/La_Paz' },\n        { text: '(GMT-04:00) Manaus', value: 'America/Manaus' },\n        { text: '(GMT-04:00) Martinique', value: 'America/Martinique' },\n        { text: '(GMT-04:00) Port of Spain', value: 'America/Port_of_Spain' },\n        { text: '(GMT-04:00) Porto Velho', value: 'America/Porto_Velho' },\n        { text: '(GMT-04:00) Puerto Rico', value: 'America/Puerto_Rico' },\n        { text: '(GMT-04:00) Santo Domingo', value: 'America/Santo_Domingo' },\n        { text: '(GMT-04:00) Thule', value: 'America/Thule' },\n        { text: '(GMT-03:30) Newfoundland Time - St. Johns', value: 'America/St_Johns' },\n        { text: '(GMT-03:00) Araguaina', value: 'America/Araguaina' },\n        { text: '(GMT-03:00) Asuncion', value: 'America/Asuncion' },\n        { text: '(GMT-03:00) Belem', value: 'America/Belem' },\n        { text: '(GMT-03:00) Buenos Aires', value: 'America/Argentina/Buenos_Aires' },\n        { text: '(GMT-03:00) Campo Grande', value: 'America/Campo_Grande' },\n        { text: '(GMT-03:00) Cayenne', value: 'America/Cayenne' },\n        { text: '(GMT-03:00) Cuiaba', value: 'America/Cuiaba' },\n        { text: '(GMT-03:00) Fortaleza', value: 'America/Fortaleza' },\n        { text: '(GMT-03:00) Godthab', value: 'America/Godthab' },\n        { text: '(GMT-03:00) Maceio', value: 'America/Maceio' },\n        { text: '(GMT-03:00) Miquelon', value: 'America/Miquelon' },\n        { text: '(GMT-03:00) Montevideo', value: 'America/Montevideo' },\n        { text: '(GMT-03:00) Palmer', value: 'Antarctica/Palmer' },\n        { text: '(GMT-03:00) Paramaribo', value: 'America/Paramaribo' },\n        { text: '(GMT-03:00) Punta Arenas', value: 'America/Punta_Arenas' },\n        { text: '(GMT-03:00) Recife', value: 'America/Recife' },\n        { text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },\n        { text: '(GMT-03:00) Salvador', value: 'America/Bahia' },\n        { text: '(GMT-03:00) Santiago', value: 'America/Santiago' },\n        { text: '(GMT-03:00) Sao Paulo', value: 'America/Sao_Paulo' },\n        { text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },\n        { text: '(GMT-02:00) Noronha', value: 'America/Noronha' },\n        { text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },\n        { text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },\n        { text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },\n        { text: '(GMT-01:00) Scoresbysund', value: 'America/Scoresbysund' },\n        { text: '(GMT+00:00) Abidjan', value: 'Africa/Abidjan' },\n        { text: '(GMT+00:00) Accra', value: 'Africa/Accra' },\n        { text: '(GMT+00:00) Bissau', value: 'Africa/Bissau' },\n        { text: '(GMT+00:00) Canary Islands', value: 'Atlantic/Canary' },\n        { text: '(GMT+00:00) Casablanca', value: 'Africa/Casablanca' },\n        { text: '(GMT+00:00) Danmarkshavn', value: 'America/Danmarkshavn' },\n        { text: '(GMT+00:00) Dublin', value: 'Europe/Dublin' },\n        { text: '(GMT+00:00) El Aaiun', value: 'Africa/El_Aaiun' },\n        { text: '(GMT+00:00) Faeroe', value: 'Atlantic/Faroe' },\n        { text: '(GMT+00:00) GMT (no daylight saving)', value: 'Etc/GMT' },\n        { text: '(GMT+00:00) Lisbon', value: 'Europe/Lisbon' },\n        { text: '(GMT+00:00) London', value: 'Europe/London' },\n        { text: '(GMT+00:00) Monrovia', value: 'Africa/Monrovia' },\n        { text: '(GMT+00:00) Reykjavik', value: 'Atlantic/Reykjavik' },\n        { text: '(GMT+01:00) Algiers', value: 'Africa/Algiers' },\n        { text: '(GMT+01:00) Amsterdam', value: 'Europe/Amsterdam' },\n        { text: '(GMT+01:00) Andorra', value: 'Europe/Andorra' },\n        { text: '(GMT+01:00) Berlin', value: 'Europe/Berlin' },\n        { text: '(GMT+01:00) Brussels', value: 'Europe/Brussels' },\n        { text: '(GMT+01:00) Budapest', value: 'Europe/Budapest' },\n        { text: '(GMT+01:00) Central European Time - Belgrade', value: 'Europe/Belgrade' },\n        { text: '(GMT+01:00) Central European Time - Prague', value: 'Europe/Prague' },\n        { text: '(GMT+01:00) Ceuta', value: 'Africa/Ceuta' },\n        { text: '(GMT+01:00) Copenhagen', value: 'Europe/Copenhagen' },\n        { text: '(GMT+01:00) Gibraltar', value: 'Europe/Gibraltar' },\n        { text: '(GMT+01:00) Lagos', value: 'Africa/Lagos' },\n        { text: '(GMT+01:00) Luxembourg', value: 'Europe/Luxembourg' },\n        { text: '(GMT+01:00) Madrid', value: 'Europe/Madrid' },\n        { text: '(GMT+01:00) Malta', value: 'Europe/Malta' },\n        { text: '(GMT+01:00) Monaco', value: 'Europe/Monaco' },\n        { text: '(GMT+01:00) Ndjamena', value: 'Africa/Ndjamena' },\n        { text: '(GMT+01:00) Oslo', value: 'Europe/Oslo' },\n        { text: '(GMT+01:00) Paris', value: 'Europe/Paris' },\n        { text: '(GMT+01:00) Rome', value: 'Europe/Rome' },\n        { text: '(GMT+01:00) Stockholm', value: 'Europe/Stockholm' },\n        { text: '(GMT+01:00) Tirane', value: 'Europe/Tirane' },\n        { text: '(GMT+01:00) Tunis', value: 'Africa/Tunis' },\n        { text: '(GMT+01:00) Vienna', value: 'Europe/Vienna' },\n        { text: '(GMT+01:00) Warsaw', value: 'Europe/Warsaw' },\n        { text: '(GMT+01:00) Zurich', value: 'Europe/Zurich' },\n        { text: '(GMT+02:00) Amman', value: 'Asia/Amman' },\n        { text: '(GMT+02:00) Athens', value: 'Europe/Athens' },\n        { text: '(GMT+02:00) Beirut', value: 'Asia/Beirut' },\n        { text: '(GMT+02:00) Bucharest', value: 'Europe/Bucharest' },\n        { text: '(GMT+02:00) Cairo', value: 'Africa/Cairo' },\n        { text: '(GMT+02:00) Chisinau', value: 'Europe/Chisinau' },\n        { text: '(GMT+02:00) Damascus', value: 'Asia/Damascus' },\n        { text: '(GMT+02:00) Gaza', value: 'Asia/Gaza' },\n        { text: '(GMT+02:00) Helsinki', value: 'Europe/Helsinki' },\n        { text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },\n        { text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },\n        { text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },\n        { text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },\n        { text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },\n        { text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },\n        { text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },\n        { text: '(GMT+02:00) Riga', value: 'Europe/Riga' },\n        { text: '(GMT+02:00) Sofia', value: 'Europe/Sofia' },\n        { text: '(GMT+02:00) Tallinn', value: 'Europe/Tallinn' },\n        { text: '(GMT+02:00) Tripoli', value: 'Africa/Tripoli' },\n        { text: '(GMT+02:00) Vilnius', value: 'Europe/Vilnius' },\n        { text: '(GMT+02:00) Windhoek', value: 'Africa/Windhoek' },\n        { text: '(GMT+03:00) Baghdad', value: 'Asia/Baghdad' },\n        { text: '(GMT+03:00) Istanbul', value: 'Europe/Istanbul' },\n        { text: '(GMT+03:00) Minsk', value: 'Europe/Minsk' },\n        { text: '(GMT+03:00) Moscow+00 - Moscow', value: 'Europe/Moscow' },\n        { text: '(GMT+03:00) Nairobi', value: 'Africa/Nairobi' },\n        { text: '(GMT+03:00) Qatar', value: 'Asia/Qatar' },\n        { text: '(GMT+03:00) Riyadh', value: 'Asia/Riyadh' },\n        { text: '(GMT+03:00) Syowa', value: 'Antarctica/Syowa' },\n        { text: '(GMT+03:30) Tehran', value: 'Asia/Tehran' },\n        { text: '(GMT+04:00) Baku', value: 'Asia/Baku' },\n        { text: '(GMT+04:00) Dubai', value: 'Asia/Dubai' },\n        { text: '(GMT+04:00) Mahe', value: 'Indian/Mahe' },\n        { text: '(GMT+04:00) Mauritius', value: 'Indian/Mauritius' },\n        { text: '(GMT+04:00) Moscow+01 - Samara', value: 'Europe/Samara' },\n        { text: '(GMT+04:00) Reunion', value: 'Indian/Reunion' },\n        { text: '(GMT+04:00) Tbilisi', value: 'Asia/Tbilisi' },\n        { text: '(GMT+04:00) Yerevan', value: 'Asia/Yerevan' },\n        { text: '(GMT+04:30) Kabul', value: 'Asia/Kabul' },\n        { text: '(GMT+05:00) Aqtau', value: 'Asia/Aqtau' },\n        { text: '(GMT+05:00) Aqtobe', value: 'Asia/Aqtobe' },\n        { text: '(GMT+05:00) Ashgabat', value: 'Asia/Ashgabat' },\n        { text: '(GMT+05:00) Dushanbe', value: 'Asia/Dushanbe' },\n        { text: '(GMT+05:00) Karachi', value: 'Asia/Karachi' },\n        { text: '(GMT+05:00) Kerguelen', value: 'Indian/Kerguelen' },\n        { text: '(GMT+05:00) Maldives', value: 'Indian/Maldives' },\n        { text: '(GMT+05:00) Mawson', value: 'Antarctica/Mawson' },\n        { text: '(GMT+05:00) Moscow+02 - Yekaterinburg', value: 'Asia/Yekaterinburg' },\n        { text: '(GMT+05:00) Tashkent', value: 'Asia/Tashkent' },\n        { text: '(GMT+05:30) Colombo', value: 'Asia/Colombo' },\n        { text: '(GMT+05:30) India Standard Time', value: 'Asia/Kolkata' },\n        { text: '(GMT+05:45) Kathmandu', value: 'Asia/Kathmandu' },\n        { text: '(GMT+06:00) Almaty', value: 'Asia/Almaty' },\n        { text: '(GMT+06:00) Bishkek', value: 'Asia/Bishkek' },\n        { text: '(GMT+06:00) Chagos', value: 'Indian/Chagos' },\n        { text: '(GMT+06:00) Dhaka', value: 'Asia/Dhaka' },\n        { text: '(GMT+06:00) Moscow+03 - Omsk', value: 'Asia/Omsk' },\n        { text: '(GMT+06:00) Thimphu', value: 'Asia/Thimphu' },\n        { text: '(GMT+06:00) Vostok', value: 'Antarctica/Vostok' },\n        { text: '(GMT+06:30) Cocos', value: 'Indian/Cocos' },\n        { text: '(GMT+06:30) Rangoon', value: 'Asia/Yangon' },\n        { text: '(GMT+07:00) Bangkok', value: 'Asia/Bangkok' },\n        { text: '(GMT+07:00) Christmas', value: 'Indian/Christmas' },\n        { text: '(GMT+07:00) Davis', value: 'Antarctica/Davis' },\n        { text: '(GMT+07:00) Hanoi', value: 'Asia/Saigon' },\n        { text: '(GMT+07:00) Hovd', value: 'Asia/Hovd' },\n        { text: '(GMT+07:00) Jakarta', value: 'Asia/Jakarta' },\n        { text: '(GMT+07:00) Moscow+04 - Krasnoyarsk', value: 'Asia/Krasnoyarsk' },\n        { text: '(GMT+08:00) Brunei', value: 'Asia/Brunei' },\n        { text: '(GMT+08:00) China Time - Beijing', value: 'Asia/Shanghai' },\n        { text: '(GMT+08:00) Choibalsan', value: 'Asia/Choibalsan' },\n        { text: '(GMT+08:00) Hong Kong', value: 'Asia/Hong_Kong' },\n        { text: '(GMT+08:00) Kuala Lumpur', value: 'Asia/Kuala_Lumpur' },\n        { text: '(GMT+08:00) Macau', value: 'Asia/Macau' },\n        { text: '(GMT+08:00) Makassar', value: 'Asia/Makassar' },\n        { text: '(GMT+08:00) Manila', value: 'Asia/Manila' },\n        { text: '(GMT+08:00) Moscow+05 - Irkutsk', value: 'Asia/Irkutsk' },\n        { text: '(GMT+08:00) Singapore', value: 'Asia/Singapore' },\n        { text: '(GMT+08:00) Taipei', value: 'Asia/Taipei' },\n        { text: '(GMT+08:00) Ulaanbaatar', value: 'Asia/Ulaanbaatar' },\n        { text: '(GMT+08:00) Western Time - Perth', value: 'Australia/Perth' },\n        { text: '(GMT+08:30) Pyongyang', value: 'Asia/Pyongyang' },\n        { text: '(GMT+09:00) Dili', value: 'Asia/Dili' },\n        { text: '(GMT+09:00) Jayapura', value: 'Asia/Jayapura' },\n        { text: '(GMT+09:00) Moscow+06 - Yakutsk', value: 'Asia/Yakutsk' },\n        { text: '(GMT+09:00) Palau', value: 'Pacific/Palau' },\n        { text: '(GMT+09:00) Seoul', value: 'Asia/Seoul' },\n        { text: '(GMT+09:00) Tokyo', value: 'Asia/Tokyo' },\n        { text: '(GMT+09:30) Central Time - Darwin', value: 'Australia/Darwin' },\n        { text: '(GMT+10:00) Dumont D\\'Urville', value: 'Antarctica/DumontDUrville' },\n        { text: '(GMT+10:00) Eastern Time - Brisbane', value: 'Australia/Brisbane' },\n        { text: '(GMT+10:00) Guam', value: 'Pacific/Guam' },\n        { text: '(GMT+10:00) Moscow+07 - Vladivostok', value: 'Asia/Vladivostok' },\n        { text: '(GMT+10:00) Port Moresby', value: 'Pacific/Port_Moresby' },\n        { text: '(GMT+10:00) Truk', value: 'Pacific/Chuuk' },\n        { text: '(GMT+10:30) Central Time - Adelaide', value: 'Australia/Adelaide' },\n        { text: '(GMT+11:00) Casey', value: 'Antarctica/Casey' },\n        { text: '(GMT+11:00) Eastern Time - Hobart', value: 'Australia/Hobart' },\n        { text: '(GMT+11:00) Eastern Time - Melbourne, Sydney', value: 'Australia/Sydney' },\n        { text: '(GMT+11:00) Efate', value: 'Pacific/Efate' },\n        { text: '(GMT+11:00) Guadalcanal', value: 'Pacific/Guadalcanal' },\n        { text: '(GMT+11:00) Kosrae', value: 'Pacific/Kosrae' },\n        { text: '(GMT+11:00) Moscow+08 - Magadan', value: 'Asia/Magadan' },\n        { text: '(GMT+11:00) Norfolk', value: 'Pacific/Norfolk' },\n        { text: '(GMT+11:00) Noumea', value: 'Pacific/Noumea' },\n        { text: '(GMT+11:00) Ponape', value: 'Pacific/Pohnpei' },\n        { text: '(GMT+12:00) Funafuti', value: 'Pacific/Funafuti' },\n        { text: '(GMT+12:00) Kwajalein', value: 'Pacific/Kwajalein' },\n        { text: '(GMT+12:00) Majuro', value: 'Pacific/Majuro' },\n        { text: '(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy', value: 'Asia/Kamchatka' },\n        { text: '(GMT+12:00) Nauru', value: 'Pacific/Nauru' },\n        { text: '(GMT+12:00) Tarawa', value: 'Pacific/Tarawa' },\n        { text: '(GMT+12:00) Wake', value: 'Pacific/Wake' },\n        { text: '(GMT+12:00) Wallis', value: 'Pacific/Wallis' },\n        { text: '(GMT+13:00) Auckland', value: 'Pacific/Auckland' },\n        { text: '(GMT+13:00) Enderbury', value: 'Pacific/Enderbury' },\n        { text: '(GMT+13:00) Fakaofo', value: 'Pacific/Fakaofo' },\n        { text: '(GMT+13:00) Fiji', value: 'Pacific/Fiji' },\n        { text: '(GMT+13:00) Tongatapu', value: 'Pacific/Tongatapu' },\n        { text: '(GMT+14:00) Apia', value: 'Pacific/Apia' },\n        { text: '(GMT+14:00) Kiritimati', value: 'Pacific/Kiritimati' }\n      ]\n    }\n  },\n  computed: {\n    currentUserId: get('user/id')\n  },\n  methods: {\n    /**\n     * Activate a user (if previously deactivated)\n     */\n    async activateUser () {\n      this.$store.commit(`loadingStart`, 'admin-users-activate')\n      const resp = await this.$apollo.mutate({\n        mutation: gql`\n          mutation ($id: Int!) {\n            users {\n              activate(id: $id) {\n                responseResult {\n                  succeeded\n                  errorCode\n                  slug\n                  message\n                }\n              }\n            }\n          }\n        `,\n        variables: {\n          id: this.user.id\n        }\n      })\n      if (_.get(resp, 'data.users.activate.responseResult.succeeded', false)) {\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:users.userActivateSuccess'),\n          icon: 'check'\n        })\n        this.user.isActive = true\n      } else {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: _.get(resp, 'data.users.activate.responseResult.message', 'An unexpected error occurred.'),\n          icon: 'warning'\n        })\n      }\n      this.$store.commit(`loadingStop`, 'admin-users-activate')\n    },\n    /**\n     * Deactivate a currently active user\n     */\n    async deactivateUser () {\n      this.$store.commit(`loadingStart`, 'admin-users-deactivate')\n      const resp = await this.$apollo.mutate({\n        mutation: gql`\n          mutation ($id: Int!) {\n            users {\n              deactivate(id: $id) {\n                responseResult {\n                  succeeded\n                  errorCode\n                  slug\n                  message\n                }\n              }\n            }\n          }\n        `,\n        variables: {\n          id: this.user.id\n        }\n      })\n      if (_.get(resp, 'data.users.deactivate.responseResult.succeeded', false)) {\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:users.userDeactivateSuccess'),\n          icon: 'check'\n        })\n        this.user.isActive = false\n      } else {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: _.get(resp, 'data.users.deactivate.responseResult.message', 'An unexpected error occurred.'),\n          icon: 'warning'\n        })\n      }\n      this.$store.commit(`loadingStop`, 'admin-users-deactivate')\n    },\n    /**\n     * Delete a user\n     */\n    deleteUserConfirm () {\n      this.deleteUserDialog = true\n      this.deleteReplaceUser = {\n        id: this.currentUserId,\n        name: this.$store.get('user/name'),\n        email: this.$store.get('user/email')\n      }\n    },\n    async deleteUser () {\n      this.$store.commit(`loadingStart`, 'admin-users-delete')\n      const resp = await this.$apollo.mutate({\n        mutation: gql`\n          mutation ($id: Int!, $replaceId: Int!) {\n            users {\n              delete(id: $id, replaceId: $replaceId) {\n                responseResult {\n                  succeeded\n                  errorCode\n                  slug\n                  message\n                }\n              }\n            }\n          }\n        `,\n        variables: {\n          id: this.user.id,\n          replaceId: this.deleteReplaceUser.id\n        }\n      })\n      if (_.get(resp, 'data.users.delete.responseResult.succeeded', false)) {\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:users.userDeleteSuccess'),\n          icon: 'check'\n        })\n        this.$router.push('/users')\n      } else {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: _.get(resp, 'data.users.delete.responseResult.message', 'An unexpected error occurred.'),\n          icon: 'warning'\n        })\n      }\n      this.deleteUserDialog = false\n      this.$store.commit(`loadingStop`, 'admin-users-delete')\n    },\n    assignDeleteUser (selUsr) {\n      if (selUsr.id === this.user.id) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: 'You cannot select the account you\\'re about to delete!',\n          icon: 'warning'\n        })\n      } else if (selUsr.id === 2) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: 'You cannot use the guest account for this operation.',\n          icon: 'warning'\n        })\n      } else {\n        this.deleteReplaceUser = selUsr\n      }\n    },\n    /**\n     * Update a user\n     */\n    async updateUser() {\n      this.$store.commit(`loadingStart`, 'admin-users-update')\n      const resp = await this.$apollo.mutate({\n        mutation: gql`\n          mutation ($id: Int!, $email: String, $name: String, $newPassword: String, $groups: [Int], $location: String, $jobTitle: String, $timezone: String) {\n            users {\n              update(id: $id, email: $email, name: $name, newPassword: $newPassword, groups: $groups, location: $location, jobTitle: $jobTitle, timezone: $timezone) {\n                responseResult {\n                  succeeded\n                  errorCode\n                  slug\n                  message\n                }\n              }\n            }\n          }\n        `,\n        variables: {\n          id: this.user.id,\n          email: this.user.email,\n          name: this.user.name,\n          newPassword: this.newPassword,\n          groups: _.map(this.user.groups, 'id'),\n          location: this.user.location,\n          jobTitle: this.user.jobTitle,\n          timezone: this.user.timezone\n        }\n      })\n      this.newPassword = ''\n      if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:users.userUpdateSuccess'),\n          icon: 'check'\n        })\n        this.$router.push('/users')\n      } else {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: _.get(resp, 'data.users.update.responseResult.message', 'An unexpected error occurred.'),\n          icon: 'warning'\n        })\n      }\n      this.$store.commit(`loadingStop`, 'admin-users-update')\n    },\n    /**\n     * Focus an input after delay\n     */\n    focusField (ipt) {\n      this.$nextTick(() => {\n        _.delay(() => {\n          this.$refs[ipt].focus()\n        }, 200)\n      })\n    },\n    /**\n     * Assign group to user\n     */\n    assignGroup() {\n      if (_.some(this.user.groups, ['id', this.newGroup])) {\n        this.$store.commit('showNotification', {\n          message: this.$t('admin:users.userAlreadyAssignedToGroup'),\n          style: 'error',\n          icon: 'alert'\n        })\n      } else {\n        this.user.groups.push(_.find(this.groups, ['id', this.newGroup]))\n        this.newGroup = 0\n      }\n    },\n    /**\n     * Unassign group from user\n     */\n    unassignGroup(gid) {\n      this.user.groups = _.reject(this.user.groups, ['id', gid])\n    },\n    /**\n     * Manually set user as verified\n     */\n    async verifyUser () {\n      this.$store.commit(`loadingStart`, 'admin-users-verify')\n      const resp = await this.$apollo.mutate({\n        mutation: gql`\n          mutation ($id: Int!) {\n            users {\n              verify(id: $id) {\n                responseResult {\n                  succeeded\n                  errorCode\n                  slug\n                  message\n                }\n              }\n            }\n          }\n        `,\n        variables: {\n          id: this.user.id\n        }\n      })\n      if (_.get(resp, 'data.users.verify.responseResult.succeeded', false)) {\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: this.$t('admin:users.userVerifySuccess'),\n          icon: 'check'\n        })\n        this.user.isVerified = true\n      } else {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: _.get(resp, 'data.users.verify.responseResult.message', 'An unexpected error occurred.'),\n          icon: 'warning'\n        })\n      }\n      this.$store.commit(`loadingStop`, 'admin-users-verify')\n    },\n    /**\n     * Toggle 2FA State\n     */\n    async toggle2FA () {\n      this.$store.commit(`loadingStart`, 'admin-users-toggle2fa')\n      if (this.user.tfaIsActive) {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($id: Int!) {\n              users {\n                disableTFA(id: $id) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.user.id\n          }\n        })\n        if (_.get(resp, 'data.users.disableTFA.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('admin:users.userTFADisableSuccess'),\n            icon: 'check'\n          })\n          this.user.tfaIsActive = false\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: _.get(resp, 'data.users.disableTFA.responseResult.message', 'An unexpected error occurred.'),\n            icon: 'warning'\n          })\n        }\n      } else {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($id: Int!) {\n              users {\n                enableTFA(id: $id) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.user.id\n          }\n        })\n        if (_.get(resp, 'data.users.enableTFA.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('admin:users.userTFAEnableSuccess'),\n            icon: 'check'\n          })\n          this.user.tfaIsActive = true\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: _.get(resp, 'data.users.enableTFA.responseResult.message', 'An unexpected error occurred.'),\n            icon: 'warning'\n          })\n        }\n      }\n      this.$store.commit(`loadingStop`, 'admin-users-toggle2fa')\n    }\n  },\n  apollo: {\n    user: {\n      query: gql`\n        query ($id: Int!) {\n          users {\n            single(id: $id) {\n              id\n              name\n              email\n              providerKey\n              providerName\n              providerId\n              providerIs2FACapable\n              location\n              jobTitle\n              timezone\n              isSystem\n              isActive\n              isVerified\n              createdAt\n              updatedAt\n              lastLoginAt\n              tfaIsActive\n              groups {\n                id\n                name\n              }\n            }\n          }\n        }\n      `,\n      variables() {\n        return {\n          id: _.toSafeInteger(this.$route.params.id)\n        }\n      },\n      fetchPolicy: 'network-only',\n      update: (data) => data.users.single,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')\n      }\n    },\n    groups: {\n      query: groupsQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-users.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-customer.svg', alt='Users', style='width: 80px;')\n          .admin-header-title\n            .headline.blue--text.text--darken-2.animated.fadeInLeft Users\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage users\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p2s.mr-3(outlined, color='grey', icon, @click='refresh')\n            v-icon mdi-refresh\n          v-btn.animated.fadeInDown(color='primary', large, depressed, @click='createUser')\n            v-icon(left) mdi-plus\n            span New User\n        v-card.mt-3.animated.fadeInUp\n          .pa-2.d-flex.align-center(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-3`')\n            v-text-field(\n              solo\n              flat\n              v-model='search'\n              prepend-inner-icon='mdi-account-search-outline'\n              label='Search Users...'\n              hide-details\n              style='max-width: 400px;'\n              dense\n              )\n            v-spacer\n            v-select(\n              solo\n              flat\n              hide-details\n              label='Identity Provider'\n              :items='strategies'\n              v-model='filterStrategy'\n              item-text='displayName'\n              item-value='key'\n              style='max-width: 300px;'\n              dense\n            )\n          v-divider\n          v-data-table(\n            v-model='selected'\n            :items='usersFiltered',\n            :headers='headers',\n            :search='search',\n            :page.sync='pagination'\n            :items-per-page='15'\n            :loading='loading'\n            @page-count='pageCount = $event'\n            hide-default-footer\n            )\n            template(slot='item', slot-scope='props')\n              tr.is-clickable(:active='props.selected', @click='$router.push(\"/users/\" + props.item.id)')\n                //- td\n                  v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected')\n                td {{ props.item.id }}\n                td: strong {{ props.item.name }}\n                td {{ props.item.email }}\n                td {{ getStrategyName(props.item.providerKey) }}\n                td {{ props.item.createdAt | moment('from') }}\n                td\n                  span(v-if='props.item.lastLoginAt') {{ props.item.lastLoginAt | moment('from') }}\n                  em.grey--text(v-else) Never\n                td.text-right\n                  v-icon.mr-3(v-if='props.item.isSystem') mdi-lock-outline\n                  status-indicator(positive, pulse, v-if='props.item.isActive')\n                  status-indicator(negative, pulse, v-else)\n            template(slot='no-data')\n              .pa-3\n                v-alert.text-left(icon='mdi-alert', outlined, color='grey')\n                  em.body-2 No users to display!\n          v-card-chin(v-if='pageCount > 1')\n            v-spacer\n            v-pagination(v-model='pagination', :length='pageCount')\n            v-spacer\n\n    user-create(v-model='isCreateDialogShown', @refresh='refresh(false)')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nimport { StatusIndicator } from 'vue-status-indicator'\nimport UserCreate from './admin-users-create.vue'\n\nexport default {\n  components: {\n    StatusIndicator,\n    UserCreate\n  },\n  data() {\n    return {\n      selected: [],\n      pagination: 1,\n      pageCount: 0,\n      users: [],\n      headers: [\n        { text: 'ID', value: 'id', width: 80, sortable: true },\n        { text: 'Name', value: 'name', sortable: true },\n        { text: 'Email', value: 'email', sortable: true },\n        { text: 'Provider', value: 'provider', sortable: true },\n        { text: 'Created', value: 'createdAt', sortable: true },\n        { text: 'Last Login', value: 'lastLoginAt', sortable: true },\n        { text: '', value: 'actions', sortable: false, width: 80 }\n      ],\n      strategies: [],\n      filterStrategy: 'all',\n      search: '',\n      loading: false,\n      isCreateDialogShown: false\n    }\n  },\n  computed: {\n    usersFiltered () {\n      const all = this.filterStrategy === 'all' || this.filterStrategy === ''\n      return _.filter(this.users, u => all || u.providerKey === this.filterStrategy)\n    }\n  },\n  methods: {\n    createUser() {\n      this.isCreateDialogShown = true\n    },\n    async refresh(notify = true) {\n      await this.$apollo.queries.users.refetch()\n      if (notify) {\n        this.$store.commit('showNotification', {\n          message: 'Users list has been refreshed.',\n          style: 'success',\n          icon: 'cached'\n        })\n      }\n    },\n    getStrategyName(key) {\n      return (_.find(this.strategies, ['key', key]) || {}).displayName || key\n    }\n  },\n  apollo: {\n    users: {\n      query: gql`\n        query {\n          users {\n            list {\n              id\n              name\n              email\n              providerKey\n              isSystem\n              isActive\n              createdAt\n              lastLoginAt\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.users.list,\n      watchLoading (isLoading) {\n        this.loading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')\n      }\n    },\n    strategies: {\n      query: gql`\n        query {\n          authentication {\n            activeStrategies {\n              key\n              displayName\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => {\n        return _.concat({\n          key: 'all',\n          displayName: 'All Providers'\n        }, data.authentication.activeStrategies)\n      },\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities-auth.vue",
    "content": "<template lang='pug'>\n  v-card\n    v-toolbar(flat, color='primary', dark, dense)\n      .subtitle-1 {{ $t('admin:utilities.authTitle') }}\n    v-card-text\n      .subtitle-1.pb-3.primary--text Generate New Authentication Public / Private Key Certificates\n      .body-2 This will invalidate all current session tokens and cause all users to be logged out.\n      .body-2.red--text You will need to log back in after the operation.\n      v-btn(outlined, color='primary', @click='regenCerts', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n      v-divider.my-5\n      .subtitle-1.pb-3.primary--text Reset Guest User\n      .body-2 This will reset the guest user to its default parameters and permissions.\n      v-btn(outlined, color='primary', @click='resetGuest', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n</template>\n\n<script>\nimport _ from 'lodash'\nimport Cookies from 'js-cookie'\nimport utilityAuthRegencertsMutation from 'gql/admin/utilities/utilities-mutation-auth-regencerts.gql'\nimport utilityAuthResetguestMutation from 'gql/admin/utilities/utilities-mutation-auth-resetguest.gql'\n\nexport default {\n  data: () => {\n    return {\n      loading: false\n    }\n  },\n  methods: {\n    async regenCerts() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-auth-regencerts')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityAuthRegencertsMutation\n        })\n        const resp = _.get(respRaw, 'data.authentication.regenerateCertificates.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: 'New Certificates generated successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n          Cookies.remove('jwt')\n          _.delay(() => {\n            window.location.assign('/login')\n          }, 1000)\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-auth-regencerts')\n      this.loading = false\n    },\n    async resetGuest() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-auth-resetguest')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityAuthResetguestMutation\n        })\n        const resp = _.get(respRaw, 'data.authentication.resetGuestUser.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: 'Guest user was reset successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-auth-resetguest')\n      this.loading = false\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities-cache.vue",
    "content": "<template lang='pug'>\n  v-card\n    v-toolbar(flat, color='primary', dark, dense)\n      .subtitle-1 {{ $t('admin:utilities.cacheTitle') }}\n    v-card-text\n      .subtitle-1.pb-3.primary--text Flush Pages and Assets Cache\n      .body-2 Pages and Assets are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.\n      v-btn(outlined, color='primary', @click='flushCache', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n      v-divider.my-5\n      .subtitle-1.pb-3.primary--text Flush Temporary Uploads\n      .body-2 New uploads are temporarily saved to disk while they are being processed. They are automatically deleted after processing, but you can force an immediate cleanup using this tool.\n      .body-2.red--text Note that performing this action while an upload is in progress can result in a failed upload.\n      v-btn(outlined, color='primary', @click='flushUploads', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n      v-divider.my-5\n      .subtitle-1.pb-3.primary--text Flush Client-Side Locale Cache\n      .body-2 Locale strings are cached in the browser local storage for 24h. You can delete your current cache in order to fetch the latest data during the next page load.\n      .body-2 Note that this affects only #[strong your own browser] and not everyone.\n      v-btn(outlined, color='primary', @click='flushClientLocaleCache', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n</template>\n\n<script>\nimport _ from 'lodash'\nimport utilityCacheFlushCacheMutation from 'gql/admin/utilities/utilities-mutation-cache-flushcache.gql'\nimport utilityCacheFlushUploadsMutation from 'gql/admin/utilities/utilities-mutation-cache-flushuploads.gql'\n\nexport default {\n  data() {\n    return {\n      loading: false\n    }\n  },\n  methods: {\n    async flushCache() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-cache-flushCache')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityCacheFlushCacheMutation\n        })\n        const resp = _.get(respRaw, 'data.pages.flushCache.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: 'Cache flushed successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-cache-flushCache')\n      this.loading = false\n    },\n    async flushUploads() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-cache-flushUploads')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityCacheFlushUploadsMutation\n        })\n        const resp = _.get(respRaw, 'data.assets.flushTempUploads.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: 'Temporary Uploads flushed successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-cache-flushUploads')\n      this.loading = false\n    },\n    async flushClientLocaleCache () {\n      for (let i = 0; i < window.localStorage.length; i++) {\n        const lsKey = window.localStorage.key(i)\n        if (_.startsWith(lsKey, 'i18next_res')) {\n          window.localStorage.removeItem(lsKey)\n        }\n      }\n      this.$store.commit('showNotification', {\n        message: 'Locale Client-Side Cache flushed successfully.',\n        style: 'success',\n        icon: 'check'\n      })\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities-content.vue",
    "content": "<template lang='pug'>\n  v-card\n    v-toolbar(flat, color='primary', dark, dense)\n      .subtitle-1 {{ $t('admin:utilities.contentTitle') }}\n    v-card-text\n      .subtitle-1.pb-3.primary--text Rebuild Page Tree\n      .body-2 The virtual structure of your wiki is automatically inferred from all page paths. You can trigger a full rebuild of the tree if some virtual folders are missing or not valid anymore.\n      v-btn(outlined, color='primary', @click='rebuildTree', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n\n      v-divider.my-5\n\n      .subtitle-1.pb-3.primary--text Rerender All Pages\n      .body-2 All pages will be rendered again. Useful if internal links are broken or the rendering pipeline has changed.\n      v-btn(outlined, color='primary', @click='rerenderPages', :disabled='loading', :loading='isRerendering').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n      v-dialog(\n        v-model='isRerendering'\n        persistent\n        max-width='450'\n        )\n        v-card(color='blue darken-2', dark)\n          v-card-text.pa-10.text-center\n            semipolar-spinner.animated.fadeIn(\n              :animation-duration='1500'\n              :size='65'\n              color='#FFF'\n              style='margin: 0 auto;'\n            )\n            .mt-5.body-1.white--text Rendering all pages...\n            .caption(v-if='renderIndex > 0') Rendering {{renderCurrentPath}}... ({{renderIndex}}/{{renderTotal}}, {{renderProgress}}%)\n            .caption.mt-4 Do not leave this page.\n            v-progress-linear.mt-5(\n              color='white'\n              :value='renderProgress'\n              stream\n              rounded\n              :buffer-value='0'\n            )\n\n      v-divider.my-5\n\n      .subtitle-1.pb-3.pl-0.primary--text Migrate all pages to target locale\n      .body-2 If you created content before selecting a different locale and activating the namespacing capabilities, you may want to transfer all content to the base locale.\n      .body-2.red--text: strong This operation is destructive and cannot be reversed! Make sure you have proper backups!\n      v-toolbar.radius-7.mt-5(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', height='80')\n        v-select(\n          label='Source Locale'\n          outlined\n          hide-details\n          :items='locales'\n          item-text='name'\n          item-value='code'\n          v-model='sourceLocale'\n        )\n        v-icon.mx-3(large) mdi-chevron-right-box-outline\n        v-select(\n          label='Target Locale'\n          outlined\n          hide-details\n          :items='locales'\n          item-text='name'\n          item-value='code'\n          v-model='targetLocale'\n        )\n      .body-2.mt-5 Pages that are already in the target locale will not be touched. If a page already exists at the target, the source page will not be modified as it would create a conflict. If you want to overwrite the target page, you must first delete it.\n      v-btn(outlined, color='primary', @click='migrateToLocale', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n\n      v-divider.my-5\n\n      .subtitle-1.pb-3.pl-0.primary--text Purge Page History\n      .body-2 You may want to purge old history for pages to reduce database usage.\n      .body-2 This operation only affects the database and not any history saved by a storage module (e.g. git version history)\n      v-toolbar.radius-7.mt-5(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', height='80')\n        v-select(\n          label='Delete history older than...'\n          outlined\n          hide-details\n          :items='purgeHistoryOptions'\n          item-text='title'\n          item-value='key'\n          v-model='purgeHistorySelection'\n        )\n      v-btn(outlined, color='primary', @click='purgeHistory', :disabled='loading').ml-0.mt-3\n        v-icon(left) mdi-gesture-double-tap\n        span Proceed\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport utilityContentMigrateLocaleMutation from 'gql/admin/utilities/utilities-mutation-content-migratelocale.gql'\nimport utilityContentRebuildTreeMutation from 'gql/admin/utilities/utilities-mutation-content-rebuildtree.gql'\n\nimport { SemipolarSpinner } from 'epic-spinners'\n\n/* global siteLangs, siteConfig */\n\nexport default {\n  components: {\n    SemipolarSpinner\n  },\n  data: () => {\n    return {\n      isRerendering: false,\n      loading: false,\n      renderProgress: 0,\n      renderIndex: 0,\n      renderTotal: 0,\n      renderCurrentPath: '',\n      sourceLocale: '',\n      targetLocale: '',\n      purgeHistorySelection: 'P1Y',\n      purgeHistoryOptions: [\n        { key: 'P1D', title: 'Today' },\n        { key: 'P1M', title: '1 month' },\n        { key: 'P3M', title: '3 months' },\n        { key: 'P6M', title: '6 months' },\n        { key: 'P1Y', title: '1 year' },\n        { key: 'P2Y', title: '2 years' },\n        { key: 'P3Y', title: '3 years' },\n        { key: 'P5Y', title: '5 years' }\n      ]\n    }\n  },\n  computed: {\n    currentLocale () {\n      return siteConfig.lang\n    },\n    locales () {\n      return siteLangs\n    }\n  },\n  methods: {\n    async rebuildTree () {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-content-rebuildtree')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityContentRebuildTreeMutation\n        })\n        const resp = _.get(respRaw, 'data.pages.rebuildTree.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: 'Page Tree rebuilt successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-content-rebuildtree')\n      this.loading = false\n    },\n    async rerenderPages () {\n      this.loading = true\n      this.isRerendering = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-content-rerender')\n\n      try {\n        const pagesRaw = await this.$apollo.query({\n          query: gql`\n            {\n              pages {\n                list {\n                  id\n                  path\n                  locale\n                }\n              }\n            }\n          `,\n          fetchPolicy: 'network-only'\n        })\n        if (_.get(pagesRaw, 'data.pages.list', []).length < 1) {\n          throw new Error('Could not find any page to render!')\n        }\n\n        this.renderIndex = 0\n        this.renderTotal = pagesRaw.data.pages.list.length\n        let failed = 0\n        for (const page of pagesRaw.data.pages.list) {\n          this.renderCurrentPath = `${page.locale}/${page.path}`\n          this.renderIndex++\n          this.renderProgress = Math.round(this.renderIndex / this.renderTotal * 100)\n          const respRaw = await this.$apollo.mutate({\n            mutation: gql`\n              mutation($id: Int!) {\n                pages {\n                  render(id: $id) {\n                    responseResult {\n                      succeeded\n                      errorCode\n                      slug\n                      message\n                    }\n                  }\n                }\n              }\n            `,\n            variables: {\n              id: page.id\n            }\n          })\n          const resp = _.get(respRaw, 'data.pages.render.responseResult', {})\n          if (!resp.succeeded) {\n            failed++\n          }\n        }\n        if (failed > 0) {\n          this.$store.commit('showNotification', {\n            message: `Completed with ${failed} pages that failed to render. Check server logs for details.`,\n            style: 'error',\n            icon: 'alert'\n          })\n        } else {\n          this.$store.commit('showNotification', {\n            message: 'All pages have been rendered successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-content-rerender')\n      this.isRerendering = false\n      this.loading = false\n    },\n    async migrateToLocale () {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-content-migratelocale')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityContentMigrateLocaleMutation,\n          variables: {\n            sourceLocale: this.sourceLocale,\n            targetLocale: this.targetLocale\n          }\n        })\n        const resp = _.get(respRaw, 'data.pages.migrateToLocale.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: `Migrated ${_.get(respRaw, 'data.pages.migrateToLocale.count', 0)} page(s) to target locale successfully.`,\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-content-migratelocale')\n      this.loading = false\n    },\n    async purgeHistory () {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-content-purgehistory')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($olderThan: String!) {\n              pages {\n                purgeHistory (\n                  olderThan: $olderThan\n                ) {\n                  responseResult {\n                    errorCode\n                    message\n                    slug\n                    succeeded\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            olderThan: this.purgeHistorySelection\n          }\n        })\n        const resp = _.get(respRaw, 'data.pages.purgeHistory.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: `Purged history successfully.`,\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-content-purgehistory')\n      this.loading = false\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities-export.vue",
    "content": "<template lang='pug'>\n  v-card\n    v-toolbar(flat, color='primary', dark, dense)\n      .subtitle-1 {{ $t('admin:utilities.exportTitle') }}\n    v-card-text\n      .text-center\n        img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-big-parcel.svg')\n        .body-2 Export to tarball / file system\n      v-divider.my-4\n      .body-2 What do you want to export?\n      v-checkbox(\n        v-for='choice of entityChoices'\n        :key='choice.key'\n        :label='choice.label'\n        :value='choice.key'\n        color='deep-orange darken-2'\n        hide-details\n        v-model='entities'\n        )\n        template(v-slot:label)\n          div\n            strong.deep-orange--text.text--darken-2 {{choice.label}}\n            .text-caption {{choice.hint}}\n      v-text-field.mt-7(\n        outlined\n        label='Target Folder Path'\n        hint='Either an absolute path or relative to the Wiki.js installation folder, where exported content will be saved to. Note that the folder MUST be empty!'\n        persistent-hint\n        v-model='filePath'\n      )\n\n      v-alert.mt-3(color='deep-orange', outlined, icon='mdi-alert', prominent)\n        .body-2 Depending on your selection, the archive could contain sensitive data such as site configuration keys and hashed user passwords. Ensure the exported archive is treated accordingly.\n        .body-2 For example, you may want to encrypt the archive if stored for backup purposes.\n\n    v-card-chin\n      v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='entities.length < 1', @click='startExport').ml-0\n        v-icon(left, color='white') mdi-database-export\n        span.white--text Start Export\n    v-dialog(\n      v-model='isLoading'\n      persistent\n      max-width='350'\n      )\n      v-card(color='deep-orange darken-2', dark)\n        v-card-text.pa-10.text-center\n          self-building-square-spinner.animated.fadeIn(\n            :animation-duration='4500'\n            :size='40'\n            color='#FFF'\n            style='margin: 0 auto;'\n          )\n          .mt-5.body-1.white--text Exporting...\n          .caption Please wait, this may take a while\n          v-progress-linear.mt-5(\n            color='white'\n            :value='progress'\n            stream\n            rounded\n            :buffer-value='0'\n          )\n    v-dialog(\n      v-model='isSuccess'\n      persistent\n      max-width='350'\n      )\n      v-card(color='green darken-2', dark)\n        v-card-text.pa-10.text-center\n          v-icon(size='60') mdi-check-circle-outline\n          .my-5.body-1.white--text Export completed\n        v-card-actions.green.darken-1\n          v-spacer\n          v-btn.px-5(\n            color='white'\n            outlined\n            @click='isSuccess = false'\n          ) Close\n          v-spacer\n    v-dialog(\n      v-model='isFailed'\n      persistent\n      max-width='800'\n      )\n      v-card(color='red darken-2', dark)\n        v-toolbar(color='red darken-2', dense)\n          v-icon mdi-alert\n          .body-2.pl-3 Export failed\n          v-spacer\n          v-btn.px-5(\n            color='white'\n            text\n            @click='isFailed = false'\n            ) Close\n        v-card-text.pa-5.red.darken-4.white--text\n          span {{errorMessage}}\n</template>\n\n<script>\nimport { SelfBuildingSquareSpinner } from 'epic-spinners'\n\nimport gql from 'graphql-tag'\nimport _get from 'lodash/get'\n\nexport default {\n  components: {\n    SelfBuildingSquareSpinner\n  },\n  data() {\n    return {\n      entities: [],\n      filePath: './data/export',\n      isLoading: false,\n      isSuccess: false,\n      isFailed: false,\n      errorMessage: '',\n      progress: 0\n    }\n  },\n  computed: {\n    entityChoices () {\n      return [\n        {\n          key: 'assets',\n          label: 'Assets',\n          hint: 'Media files such as images, documents, etc.'\n        },\n        {\n          key: 'comments',\n          label: 'Comments',\n          hint: 'Comments made using the default comment module only.'\n        },\n        {\n          key: 'navigation',\n          label: 'Navigation',\n          hint: 'Sidebar links when using Static or Custom Navigation.'\n        },\n        {\n          key: 'pages',\n          label: 'Pages',\n          hint: 'Page content, tags and related metadata.'\n        },\n        {\n          key: 'history',\n          label: 'Pages History',\n          hint: 'All previous versions of pages and their related metadata.'\n        },\n        {\n          key: 'settings',\n          label: 'Settings',\n          hint: 'Site configuration and modules settings.'\n        },\n        {\n          key: 'groups',\n          label: 'User Groups',\n          hint: 'Group permissions and page rules.'\n        },\n        {\n          key: 'users',\n          label: 'Users',\n          hint: 'Users metadata and their group memberships.'\n        }\n      ]\n    }\n  },\n  methods: {\n    async checkProgress () {\n      try {\n        const respStatus = await this.$apollo.query({\n          query: gql`\n            {\n              system {\n                exportStatus {\n                  status\n                  progress\n                  message\n                  startedAt\n                }\n              }\n            }\n          `,\n          fetchPolicy: 'network-only'\n        })\n        const respStatusObj = _get(respStatus, 'data.system.exportStatus', {})\n        if (!respStatusObj) {\n          throw new Error('An unexpected error occured.')\n        } else {\n          switch (respStatusObj.status) {\n            case 'error': {\n              throw new Error(respStatusObj.message || 'An unexpected error occured.')\n            }\n            case 'running': {\n              this.progress = respStatusObj.progress || 0\n              window.requestAnimationFrame(() => {\n                setTimeout(() => {\n                  this.checkProgress()\n                }, 5000)\n              })\n              break\n            }\n            case 'success': {\n              this.isLoading = false\n              this.isSuccess = true\n              break\n            }\n            default: {\n              throw new Error('Invalid export status.')\n            }\n          }\n        }\n      } catch (err) {\n        this.errorMessage = err.message\n        this.isLoading = false\n        this.isFailed = true\n      }\n    },\n    async startExport () {\n      this.isFailed = false\n      this.isSuccess = false\n      this.isLoading = true\n      this.progress = 0\n\n      setTimeout(async () => {\n        try {\n          // -> Initiate export\n          const respExport = await this.$apollo.mutate({\n            mutation: gql`\n              mutation (\n                $entities: [String]!\n                $path: String!\n              ) {\n                system {\n                  export (\n                    entities: $entities\n                    path: $path\n                  ) {\n                    responseResult {\n                      succeeded\n                      message\n                    }\n                  }\n                }\n              }\n            `,\n            variables: {\n              entities: this.entities,\n              path: this.filePath\n            }\n          })\n\n          const respExportObj = _get(respExport, 'data.system.export', {})\n          if (!_get(respExportObj, 'responseResult.succeeded', false)) {\n            this.errorMessage = _get(respExportObj, 'responseResult.message', 'An unexpected error occurred')\n            this.isLoading = false\n            this.isFailed = true\n            return\n          }\n\n          // -> Check for progress\n          this.checkProgress()\n        } catch (err) {\n          this.$store.commit('pushGraphError', err)\n          this.isLoading = false\n        }\n      }, 1500)\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities-importv1.vue",
    "content": "<template lang='pug'>\n  v-card\n    v-toolbar(flat, color='primary', dark, dense)\n      .subtitle-1 {{ $t('admin:utilities.importv1Title') }}\n    v-card-text\n      .text-center\n        img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')\n        .body-2 Import from Wiki.js 1.x\n      v-divider.my-4\n      .body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?\n      v-checkbox(\n        label='Content + Uploads'\n        value='content'\n        color='deep-orange darken-2'\n        v-model='importFilters'\n        hide-details\n        )\n        template(v-slot:label)\n          strong.deep-orange--text.text--darken-2 Content + Uploads\n      .pl-8(v-if='wantContent')\n        v-radio-group(v-model='contentMode', hide-details)\n          v-radio(\n            value='git'\n            color='primary'\n            )\n            template(v-slot:label)\n              div\n                span Import from Git Connection\n                .caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.\n        .pl-8.mt-5(v-if='needGit')\n          v-row\n            v-col(cols='8')\n              v-select(\n                label='Authentication Mode'\n                :items='gitAuthModes'\n                v-model='gitAuthMode'\n                outlined\n                hide-details\n              )\n            v-col(cols='4')\n              v-switch(\n                label='Verify SSL Certificate'\n                v-model='gitVerifySSL'\n                hide-details\n                color='primary'\n              )\n            v-col(cols='8')\n              v-text-field(\n                outlined\n                label='Repository URL'\n                :placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'\n                hide-details\n                v-model='gitRepoUrl'\n              )\n            v-col(cols='4')\n              v-text-field(\n                label='Branch'\n                placeholder='e.g. master'\n                v-model='gitRepoBranch'\n                outlined\n                hide-details\n              )\n            v-col(v-if='gitAuthMode === `ssh`', cols='12')\n              v-textarea(\n                outlined\n                label='Private Key Contents'\n                placeholder='-----BEGIN RSA PRIVATE KEY-----\\n...\\n-----END RSA PRIVATE KEY-----'\n                hide-details\n                v-model='gitPrivKey'\n              )\n            template(v-else-if='gitAuthMode === `basic`')\n              v-col(cols='6')\n                v-text-field(\n                  label='Username'\n                  v-model='gitUsername'\n                  outlined\n                  hide-details\n                )\n              v-col(cols='6')\n                v-text-field(\n                  type='password'\n                  label='Password / PAT'\n                  v-model='gitPassword'\n                  outlined\n                  hide-details\n                )\n            v-col(cols='6')\n              v-text-field(\n                label='Default Author Email'\n                placeholder='e.g. name@company.com'\n                v-model='gitUserEmail'\n                outlined\n                hide-details\n              )\n            v-col(cols='6')\n              v-text-field(\n                label='Default Author Name'\n                placeholder='e.g. John Smith'\n                v-model='gitUserName'\n                outlined\n                hide-details\n              )\n            v-col(cols='12')\n              v-text-field(\n                label='Local Repository Path'\n                placeholder='e.g. ./data/repo'\n                v-model='gitRepoPath'\n                outlined\n                hide-details\n              )\n              .caption.mt-2 This folder should be empty or not exist yet. #[strong.deep-orange--text.text--darken-2 DO NOT] point to your existing Wiki.js 1.x repository folder. In most cases, it should be left to the default value.\n          v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)\n            .body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.\n            .body-2 - Although both v1 and v2 installations can use the same remote git repository, you shouldn't make edits to the same pages simultaneously.\n        v-radio-group(v-model='contentMode', hide-details)\n          v-divider\n          v-radio.mt-3(\n            value='disk'\n            color='primary'\n            )\n            template(v-slot:label)\n              div\n                span Import from local folder\n                .caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.\n        .pl-8.mt-5(v-if='needDisk')\n          v-text-field(\n            outlined\n            label='Content Repo Path'\n            hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'\n            persistent-hint\n            v-model='contentPath'\n          )\n\n      v-checkbox(\n        label='Users'\n        value='users'\n        color='deep-orange darken-2'\n        v-model='importFilters'\n        hide-details\n        )\n        template(v-slot:label)\n          strong.deep-orange--text.text--darken-2 Users\n      .pl-8.mt-5(v-if='wantUsers')\n        v-text-field(\n          outlined\n          label='MongoDB Connection String'\n          hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'\n          persistent-hint\n          v-model='dbConnStr'\n        )\n        v-radio-group(v-model='groupMode', hide-details, mandatory)\n          v-radio(\n            value='MULTI'\n            color='primary'\n            )\n            template(v-slot:label)\n              div\n                span Create groups for each unique user permissions configuration\n                .caption: em #[strong.primary--text Recommended] | Users having identical permission sets will be assigned to the same group. Note that this can potentially result in a large amount of groups being created.\n          v-divider\n          v-radio.mt-3(\n            value='SINGLE'\n            color='primary'\n            )\n            template(v-slot:label)\n              div\n                span Create a single group with all imported users\n                .caption: em The new group will have read permissions enabled by default.\n          v-divider\n          v-radio.mt-3(\n            value='NONE'\n            color='primary'\n            )\n            template(v-slot:label)\n              div\n                span Don't create any group\n                .caption: em Users will not be able to access your wiki until they are assigned to a group.\n\n        v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)\n          .body-2 Note that any user that already exists in this installation will not be imported. A list of skipped users will be displayed upon completion.\n          .caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.\n\n    v-card-chin\n      v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0\n        v-icon(left, color='white') mdi-database-import\n        span.white--text Start Import\n    v-dialog(\n      v-model='isLoading'\n      persistent\n      max-width='350'\n      )\n      v-card(color='deep-orange darken-2', dark)\n        v-card-text.pa-10.text-center\n          semipolar-spinner.animated.fadeIn(\n            :animation-duration='1500'\n            :size='65'\n            color='#FFF'\n            style='margin: 0 auto;'\n          )\n          .mt-5.body-1.white--text Importing from Wiki.js 1.x...\n          .caption Please wait\n          v-progress-linear.mt-5(\n            color='white'\n            :value='progress'\n            stream\n            rounded\n            :buffer-value='0'\n          )\n    v-dialog(\n      v-model='isSuccess'\n      persistent\n      max-width='350'\n      )\n      v-card(color='green darken-2', dark)\n        v-card-text.pa-10.text-center\n          v-icon(size='60') mdi-check-circle-outline\n          .my-5.body-1.white--text Import completed\n          template(v-if='wantUsers')\n            .body-2\n              span #[strong {{successUsers}}] users imported\n              v-btn.text-none.ml-3(\n                v-if='failedUsers.length > 0'\n                text\n                color='white'\n                dark\n                @click='showFailedUsers = true'\n                )\n                v-icon(left) mdi-alert\n                span {{failedUsers.length}} failed\n            .body-2 #[strong {{successGroups}}] groups created\n        v-card-actions.green.darken-1\n          v-spacer\n          v-btn.px-5(\n            color='white'\n            outlined\n            @click='isSuccess = false'\n          ) Close\n          v-spacer\n    v-dialog(\n      v-model='showFailedUsers'\n      persistent\n      max-width='800'\n      )\n      v-card(color='red darken-2', dark)\n        v-toolbar(color='red darken-2', dense)\n          v-icon mdi-alert\n          .body-2.pl-3 Failed User Imports\n          v-spacer\n          v-btn.px-5(\n            color='white'\n            text\n            @click='showFailedUsers = false'\n            ) Close\n        v-simple-table(dense, fixed-header, height='300px')\n          template(v-slot:default)\n            thead\n              tr\n                th Provider\n                th Email\n                th Error\n            tbody\n              tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')\n                td {{fusr.provider}}\n                td {{fusr.email}}\n                td {{fusr.error}}\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport { SemipolarSpinner } from 'epic-spinners'\n\nimport utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'\nimport storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'\nimport storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'\nimport targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'\nimport targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'\n\nexport default {\n  components: {\n    SemipolarSpinner\n  },\n  data() {\n    return {\n      importFilters: ['content', 'users'],\n      groupMode: 'MULTI',\n      contentMode: 'git',\n      dbConnStr: 'mongodb://',\n      contentPath: '/wiki-v1/repo',\n      isLoading: false,\n      isSuccess: false,\n      gitAuthMode: 'ssh',\n      gitAuthModes: [\n        { text: 'SSH', value: 'ssh' },\n        { text: 'Basic', value: 'basic' }\n      ],\n      gitVerifySSL: true,\n      gitRepoUrl: '',\n      gitRepoBranch: 'master',\n      gitPrivKey: '',\n      gitUsername: '',\n      gitPassword: '',\n      gitUserEmail: '',\n      gitUserName: '',\n      gitRepoPath: './data/repo',\n      progress: 0,\n      successGroups: 0,\n      successUsers: 0,\n      successPages: 0,\n      showFailedUsers: false,\n      failedUsers: []\n    }\n  },\n  computed: {\n    wantContent () {\n      return this.importFilters.indexOf('content') >= 0\n    },\n    wantUsers () {\n      return this.importFilters.indexOf('users') >= 0\n    },\n    needDisk () {\n      return this.contentMode === `disk`\n    },\n    needGit () {\n      return this.contentMode === `git`\n    }\n  },\n  methods: {\n    async startImport () {\n      this.isLoading = true\n      this.progress = 0\n      this.failedUsers = []\n\n      _.delay(async () => {\n        // -> Import Users\n\n        if (this.wantUsers) {\n          try {\n            const resp = await this.$apollo.mutate({\n              mutation: utilityImportv1UsersMutation,\n              variables: {\n                mongoDbConnString: this.dbConnStr,\n                groupMode: this.groupMode\n              }\n            })\n            const respObj = _.get(resp, 'data.system.importUsersFromV1', {})\n            if (!_.get(respObj, 'responseResult.succeeded', false)) {\n              throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))\n            }\n            this.successUsers = _.get(respObj, 'usersCount', 0)\n            this.successGroups = _.get(respObj, 'groupsCount', 0)\n            this.failedUsers = _.get(respObj, 'failed', [])\n            this.progress += 50\n          } catch (err) {\n            this.$store.commit('pushGraphError', err)\n            this.isLoading = false\n            return\n          }\n        }\n\n        // -> Import Content\n\n        if (this.wantContent) {\n          try {\n            const resp = await this.$apollo.query({\n              query: storageTargetsQuery,\n              fetchPolicy: 'network-only'\n            })\n            if (_.has(resp, 'data.storage.targets')) {\n              this.progress += 10\n              let targets = resp.data.storage.targets.map(str => {\n                let nStr = {\n                  ...str,\n                  config: _.sortBy(str.config.map(cfg => ({\n                    ...cfg,\n                    value: JSON.parse(cfg.value)\n                  })), [t => t.value.order])\n                }\n\n                // -> Setup Git Module\n\n                if (this.contentMode === 'git' && nStr.key === 'git') {\n                  nStr.isEnabled = true\n                  nStr.mode = 'sync'\n                  nStr.syncInterval = 'PT5M'\n                  nStr.config = [\n                    { key: 'authType', value: { value: this.gitAuthMode } },\n                    { key: 'repoUrl', value: { value: this.gitRepoUrl } },\n                    { key: 'branch', value: { value: this.gitRepoBranch } },\n                    { key: 'sshPrivateKeyMode', value: { value: 'contents' } },\n                    { key: 'sshPrivateKeyPath', value: { value: '' } },\n                    { key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },\n                    { key: 'verifySSL', value: { value: this.gitVerifySSL } },\n                    { key: 'basicUsername', value: { value: this.gitUsername } },\n                    { key: 'basicPassword', value: { value: this.gitPassword } },\n                    { key: 'defaultEmail', value: { value: this.gitUserEmail } },\n                    { key: 'defaultName', value: { value: this.gitUserName } },\n                    { key: 'localRepoPath', value: { value: this.gitRepoPath } },\n                    { key: 'gitBinaryPath', value: { value: '' } }\n                  ]\n                }\n\n                // -> Setup Disk Module\n                if (this.contentMode === 'disk' && nStr.key === 'disk') {\n                  nStr.isEnabled = true\n                  nStr.mode = 'push'\n                  nStr.syncInterval = 'P0D'\n                  nStr.config = [\n                    { key: 'path', value: { value: this.contentPath } },\n                    { key: 'createDailyBackups', value: { value: false } }\n                  ]\n                }\n                return nStr\n              })\n\n              // -> Save storage modules configuration\n\n              const respSv = await this.$apollo.mutate({\n                mutation: targetsSaveMutation,\n                variables: {\n                  targets: targets.map(tgt => _.pick(tgt, [\n                    'isEnabled',\n                    'key',\n                    'config',\n                    'mode',\n                    'syncInterval'\n                  ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))\n                }\n              })\n              const respObj = _.get(respSv, 'data.storage.updateTargets', {})\n              if (!_.get(respObj, 'responseResult.succeeded', false)) {\n                throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))\n              }\n\n              this.progress += 10\n\n              // -> Wait for success sync\n\n              let statusAttempts = 0\n              while (statusAttempts < 10) {\n                statusAttempts++\n                const respStatus = await this.$apollo.query({\n                  query: storageStatusQuery,\n                  fetchPolicy: 'network-only'\n                })\n                if (_.has(respStatus, 'data.storage.status[0]')) {\n                  const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])\n                  if (!st) {\n                    throw new Error('Storage target could not be configured.')\n                  }\n                  switch (st.status) {\n                    case 'pending':\n                      if (statusAttempts >= 10) {\n                        throw new Error('Storage target is stuck in pending state. Try again.')\n                      } else {\n                        continue\n                      }\n                    case 'operational':\n                      statusAttempts = 10\n                      break\n                    case 'error':\n                      throw new Error(st.message)\n                  }\n                } else {\n                  throw new Error('Failed to fetch storage sync status.')\n                }\n              }\n\n              this.progress += 15\n\n              // -> Perform import all\n\n              const respImport = await this.$apollo.mutate({\n                mutation: targetExecuteActionMutation,\n                variables: {\n                  targetKey: this.contentMode,\n                  handler: 'importAll'\n                }\n              })\n\n              const respImportObj = _.get(respImport, 'data.storage.executeAction', {})\n              if (!_.get(respImportObj, 'responseResult.succeeded', false)) {\n                throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occurred'))\n              }\n\n              this.progress += 15\n            } else {\n              throw new Error('Failed to fetch storage targets.')\n            }\n          } catch (err) {\n            this.$store.commit('pushGraphError', err)\n            this.isLoading = false\n            return\n          }\n        }\n\n        this.isLoading = false\n        this.isSuccess = true\n      }, 1500)\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities-telemetry.vue",
    "content": "<template lang='pug'>\n  v-card\n    v-toolbar(flat, color='primary', dark, dense)\n      .subtitle-1 {{ $t('admin:utilities.telemetryTitle') }}\n    v-form\n      v-card-text\n        .subtitle-2 What is telemetry?\n        .body-2.mt-3 Telemetry allows the developers of Wiki.js to improve the software by collecting basic anonymized data about its usage and the host info. #[br] This is entirely optional and #[strong absolutely no] private data (such as content or personal data) is collected.\n        .body-2.mt-3 For maximum privacy, a random client ID is generated during setup. This ID is used to group requests together while keeping complete anonymity. You can reset and generate a new one below at any time.\n        v-divider.my-4\n        .subtitle-2 What is collected?\n        .body-2.mt-3 When telemetry is enabled, only the following data is transmitted:\n        v-list\n          v-list-item\n            v-list-item-avatar: v-icon mdi-information-outline\n            v-list-item-content\n              v-list-item-title.body-2 Version of Wiki.js installed\n              v-list-item-subtitle.caption: em e.g. v2.0.123\n          v-list-item\n            v-list-item-avatar: v-icon mdi-information-outline\n            v-list-item-content\n              v-list-item-title.body-2 Basic OS information\n              v-list-item-subtitle.caption: em Platform (Linux, macOS or Windows), Total CPU cores and DB type (PostgreSQL, MySQL, MariaDB, SQLite or SQL Server)\n          v-list-item\n            v-list-item-avatar: v-icon mdi-information-outline\n            v-list-item-content\n              v-list-item-title.body-2 Crash debug data\n              v-list-item-subtitle.caption: em Stack trace of the error\n          v-list-item\n            v-list-item-avatar: v-icon mdi-information-outline\n            v-list-item-content\n              v-list-item-title.body-2 Setup analytics\n              v-list-item-subtitle.caption: em Installation checkpoint reached\n        .body-2 Note that crash debug data is stored for a maximum of 30 days while analytics are stored for a maximum of 16 months, after which it is permanently deleted.\n        v-divider.my-4\n        .subtitle-2 What is it used for?\n        .body-2.mt-3 Telemetry is used by developers to improve Wiki.js, mostly for the following reasons:\n        v-list(dense)\n          v-list-item\n            v-list-item-avatar: v-icon mdi-chevron-right\n            v-list-item-content: v-list-item-title: .body-2 Identify critical bugs more easily and fix them in a timely manner.\n          v-list-item\n            v-list-item-avatar: v-icon mdi-chevron-right\n            v-list-item-content: v-list-item-title: .body-2 Understand the upgrade rate of current installations.\n          v-list-item\n            v-list-item-avatar: v-icon mdi-chevron-right\n            v-list-item-content: v-list-item-title: .body-2  Optimize performance and testing scenarios based on most popular environments.\n        .body-2 Only authorized developers have access to the data. It is not shared to any 3rd party nor is it used for any other application than improving Wiki.js.\n        v-divider.my-4\n        .subtitle-2 Settings\n        .mt-3\n          v-switch.mt-0(\n            v-model='telemetry',\n            label='Enable Telemetry',\n            color='primary',\n            hint='Allow Wiki.js to transmit telemetry data.',\n            persistent-hint\n          )\n        v-divider.my-4\n        .subtitle-2.mt-3.grey--text.text--darken-1 Client ID\n        .body-2.mt-2 {{clientId}}\n      v-card-chin\n        v-btn.px-3(depressed, color='success', @click='updateTelemetry')\n          v-icon(left) mdi-chevron-right\n          | Save Changes\n        v-spacer\n        v-btn.px-3(outlined, color='grey', @click='resetClientId')\n          v-icon(left) mdi-autorenew\n          span Reset Client ID\n\n</template>\n\n<script>\nimport _ from 'lodash'\n\nimport utilityTelemetryResetIdMutation from 'gql/admin/utilities/utilities-mutation-telemetry-resetid.gql'\nimport utilityTelemetrySetMutation from 'gql/admin/utilities/utilities-mutation-telemetry-set.gql'\nimport utilityTelemetryQuery from 'gql/admin/utilities/utilities-query-telemetry.gql'\n\nexport default {\n  data() {\n    return {\n      telemetry: false,\n      clientId: 'N/A'\n    }\n  },\n  methods: {\n    async updateTelemetry() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-telemetry-set')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityTelemetrySetMutation,\n          variables: {\n            enabled: this.telemetry\n          }\n        })\n        const resp = _.get(respRaw, 'data.system.setTelemetry.responseResult', {})\n        if (resp.succeeded) {\n          this.$store.commit('showNotification', {\n            message: 'Telemetry updated successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-telemetry-set')\n      this.loading = false\n    },\n    async resetClientId() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'admin-utilities-telemetry-resetid')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: utilityTelemetryResetIdMutation\n        })\n        const resp = _.get(respRaw, 'data.system.resetTelemetryClientId.responseResult', {})\n        if (resp.succeeded) {\n          this.$apollo.queries.telemetry.refetch()\n          this.$store.commit('showNotification', {\n            message: 'Telemetry Client ID reset successfully.',\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'admin-utilities-telemetry-resetid')\n      this.loading = false\n    }\n  },\n  apollo: {\n    telemetry: {\n      query: utilityTelemetryQuery,\n      fetchPolicy: 'network-only',\n      manual: true,\n      result ({ data }) {\n        this.telemetry = _.get(data, 'system.info.telemetry', false)\n        this.clientId = _.get(data, 'system.info.telemetryClientId', 'N/A')\n      },\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-utilities-telemetry-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-utilities.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img(src='/_assets/svg/icon-maintenance.svg', alt='Utilities', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text {{$t('admin:utilities.title')}}\n            .subtitle-1.grey--text {{$t('admin:utilities.subtitle')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='primary', dark, dense)\n            .subtitle-1 {{$t('admin:utilities.tools')}}\n          v-list(two-line, dense).py-0\n            template(v-for='(tool, idx) in tools')\n              v-list-item(:key='tool.key', @click='selectedTool = tool.key', :disabled='!tool.isAvailable')\n                v-list-item-avatar\n                  v-icon(:color='!tool.isAvailable ? `grey lighten-1` : (selectedTool === tool.key ? `blue ` : `grey darken-1`)') {{ tool.icon }}\n                v-list-item-content\n                  v-list-item-title.body-2(:class='!tool.isAvailable ? `grey--text` : (selectedTool === tool.key ? `primary--text` : ``)') {{ $t('admin:utilities.' + tool.i18nKey + 'Title') }}\n                  v-list-item-subtitle: .caption(:class='!tool.isAvailable ? `grey--text text--lighten-1` : (selectedTool === tool.key ? `blue--text ` : ``)') {{ $t('admin:utilities.' + tool.i18nKey + 'Subtitle') }}\n                v-list-item-avatar(v-if='selectedTool === tool.key')\n                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right\n              v-divider(v-if='idx < tools.length - 1')\n\n      v-flex.animated.fadeInUp.wait-p2s(xs12, lg9)\n        transition(name='admin-router')\n          component(:is='selectedTool')\n\n</template>\n\n<script>\n\nexport default {\n  components: {\n    UtilityAuth: () => import(/* webpackChunkName: \"admin\" */ './admin-utilities-auth.vue'),\n    UtilityContent: () => import(/* webpackChunkName: \"admin\" */ './admin-utilities-content.vue'),\n    UtilityCache: () => import(/* webpackChunkName: \"admin\" */ './admin-utilities-cache.vue'),\n    UtilityExport: () => import(/* webpackChunkName: \"admin\" */ './admin-utilities-export.vue'),\n    UtilityImportv1: () => import(/* webpackChunkName: \"admin\" */ './admin-utilities-importv1.vue'),\n    UtilityTelemetry: () => import(/* webpackChunkName: \"admin\" */ './admin-utilities-telemetry.vue')\n  },\n  data() {\n    return {\n      selectedTool: 'UtilityAuth',\n      tools: [\n        {\n          key: 'UtilityAuth',\n          icon: 'mdi-lock-open-outline',\n          i18nKey: 'auth',\n          isAvailable: true\n        },\n        {\n          key: 'UtilityContent',\n          icon: 'mdi-content-duplicate',\n          i18nKey: 'content',\n          isAvailable: true\n        },\n        {\n          key: 'UtilityExport',\n          icon: 'mdi-database-export',\n          i18nKey: 'export',\n          isAvailable: true\n        },\n        {\n          key: 'UtilityCache',\n          icon: 'mdi-database-refresh',\n          i18nKey: 'cache',\n          isAvailable: true\n        },\n        // {\n        //   key: 'UtilityGraphEndpoint',\n        //   icon: 'mdi-graphql',\n        //   i18nKey: 'graphEndpoint',\n        //   isAvailable: false\n        // },\n        {\n          key: 'UtilityImportv1',\n          icon: 'mdi-database-import',\n          i18nKey: 'importv1',\n          isAvailable: true\n        },\n        {\n          key: 'UtilityTelemetry',\n          icon: 'mdi-math-compass',\n          i18nKey: 'telemetry',\n          isAvailable: true\n        }\n      ]\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin/admin-webhooks.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row, wrap)\n      v-flex(xs12)\n        .admin-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-winter.svg', alt='Mail', style='width: 80px;')\n          .admin-header-title\n            .headline.primary--text.animated.fadeInLeft {{ $t('admin:webhooks.title') }}\n            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:webhooks.subtitle') }}\n          v-spacer\n          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large, disabled)\n            v-icon(left) check\n            span {{$t('common:actions.apply')}}\n\n      v-flex(lg3, xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(flat, color='primary', dark, dense)\n            .subtitle-1 Webhooks\n            v-spacer\n            v-btn(outline, small)\n              v-icon.mr-2 add\n              span New\n          v-list(two-line, dense).py-0\n            template(v-for='(str, idx) in hooks')\n              v-list-item(:key='str.key', @click='selectedHook = str.key')\n                v-list-item-avatar\n                  v-icon(color='primary', v-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') check_box\n                  v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') check_box_outline_blank\n                v-list-item-content\n                  v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedHook === str.key ? `primary--text` : ``)') {{ str.title }}\n                  v-list-item-sub-title.caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedHook === str.key ? `blue--text ` : ``)') {{ str.description }}\n                v-list-item-avatar(v-if='selectedHook === str.key')\n                  v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios\n              v-divider(v-if='idx < hooks.length - 1')\n\n      v-flex(xs12, lg9)\n        v-card.wiki-form.animated.fadeInUp.wait-p2s\n          v-toolbar(color='primary', dense, flat, dark)\n            .subtitle-1 {{hook.title}}\n          v-card-text\n            v-form\n              .authlogo\n                img(:src='hook.logo', :alt='hook.title')\n              .caption.pt-3 {{hook.description}}\n              .caption.pb-3: a(:href='hook.website') {{hook.website}}\n              .body-2(v-if='hook.isEnabled')\n                span This hook is\n\n</template>\n\n<script>\nimport _ from 'lodash'\n// import { get } from 'vuex-pathify'\nimport mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'\nimport mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'\n\nexport default {\n  data() {\n    return {\n      hooks: [],\n      selectedHook: ''\n    }\n  },\n  computed: {\n    hook() {\n      return _.find(this.hooks, ['id', this.selectedHook]) || {}\n    }\n  },\n  methods: {\n    async save () {\n      try {\n        await this.$apollo.mutate({\n          mutation: mailUpdateConfigMutation,\n          variables: {\n            senderName: this.config.senderName || '',\n            senderEmail: this.config.senderEmail || '',\n            host: this.config.host || '',\n            port: _.toSafeInteger(this.config.port) || 0,\n            secure: this.config.secure || false,\n            user: this.config.user || '',\n            pass: this.config.pass || '',\n            useDKIM: this.config.useDKIM || false,\n            dkimDomainName: this.config.dkimDomainName || '',\n            dkimKeySelector: this.config.dkimKeySelector || '',\n            dkimPrivateKey: this.config.dkimPrivateKey || ''\n          },\n          watchLoading (isLoading) {\n            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')\n          }\n        })\n        this.$store.commit('showNotification', {\n          style: 'success',\n          message: 'Configuration saved successfully.',\n          icon: 'check'\n        })\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n    }\n  },\n  apollo: {\n    hooks: {\n      query: mailConfigQuery,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.mail.config),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/admin.vue",
    "content": "<template lang='pug'>\n  v-app.admin\n    nav-header(hide-search)\n      template(slot='mid')\n        v-spacer\n        .overline.grey--text {{$t('admin:adminArea')}}\n        v-spacer\n    v-navigation-drawer.pb-0.admin-sidebar(v-model='adminDrawerShown', app, fixed, clipped, :right='$vuetify.rtl', permanent, width='300', :class='$vuetify.theme.dark ? `grey darken-4` : ``')\n      vue-scroll(:ops='scrollStyle')\n        v-list.radius-0(dense, nav)\n          v-list-item(to='/dashboard', color='primary')\n            v-list-item-avatar(size='24', tile): v-icon mdi-view-dashboard-variant\n            v-list-item-title {{ $t('admin:dashboard.title') }}\n          template(v-if='hasPermission([`manage:system`, `manage:navigation`, `write:pages`, `manage:pages`, `delete:pages`])')\n            v-divider.my-2\n            v-subheader.pl-4 {{ $t('admin:nav.site') }}\n            v-list-item(to='/general', color='primary', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-widgets\n              v-list-item-title {{ $t('admin:general.title') }}\n            v-list-item(to='/locale', color='primary', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-web\n              v-list-item-title {{ $t('admin:locale.title') }}\n            v-list-item(to='/navigation', color='primary', v-if='hasPermission([`manage:system`, `manage:navigation`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-near-me\n              v-list-item-title {{ $t('admin:navigation.title') }}\n            v-list-item(to='/pages', color='primary', v-if='hasPermission([`manage:system`, `write:pages`, `manage:pages`, `delete:pages`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-file-document-outline\n              v-list-item-title {{ $t('admin:pages.title') }}\n              v-list-item-action(style='min-width:auto;')\n                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-5`')\n                  .caption.grey--text {{ info.pagesTotal }}\n            v-list-item(to='/tags', v-if='hasPermission([`manage:system`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-tag-multiple\n              v-list-item-title {{ $t('admin:tags.title') }}\n              v-list-item-action(style='min-width:auto;')\n                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-5`')\n                  .caption.grey--text {{ info.tagsTotal }}\n            v-list-item(to='/theme', color='primary', v-if='hasPermission([`manage:system`, `manage:theme`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-palette-outline\n              v-list-item-title {{ $t('admin:theme.title') }}\n          template(v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')\n            v-divider.my-2\n            v-subheader.pl-4 {{ $t('admin:nav.users') }}\n            v-list-item(to='/groups', color='primary', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-account-group\n              v-list-item-title {{ $t('admin:groups.title') }}\n              v-list-item-action(style='min-width:auto;')\n                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-4`')\n                  .caption.grey--text {{ info.groupsTotal }}\n            v-list-item(to='/users', color='primary', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-account-box\n              v-list-item-title {{ $t('admin:users.title') }}\n              v-list-item-action(style='min-width:auto;')\n                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-4`')\n                  .caption.grey--text {{ info.usersTotal }}\n          template(v-if='hasPermission(`manage:system`)')\n            v-divider.my-2\n            v-subheader.pl-4 {{ $t('admin:nav.modules') }}\n            v-list-item(to='/analytics', color='primary')\n              v-list-item-avatar(size='24', tile): v-icon mdi-chart-timeline-variant\n              v-list-item-title {{ $t('admin:analytics.title') }}\n            v-list-item(to='/auth', color='primary')\n              v-list-item-avatar(size='24', tile): v-icon mdi-lock-outline\n              v-list-item-title {{ $t('admin:auth.title') }}\n            v-list-item(to='/comments')\n              v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline\n              v-list-item-title {{ $t('admin:comments.title') }}\n            v-list-item(to='/rendering', color='primary')\n              v-list-item-avatar(size='24', tile): v-icon mdi-cogs\n              v-list-item-title {{ $t('admin:rendering.title') }}\n            v-list-item(to='/search', color='primary')\n              v-list-item-avatar(size='24', tile): v-icon mdi-cloud-search-outline\n              v-list-item-title {{ $t('admin:search.title') }}\n            v-list-item(to='/storage', color='primary')\n              v-list-item-avatar(size='24', tile): v-icon mdi-harddisk\n              v-list-item-title {{ $t('admin:storage.title') }}\n          template(v-if='hasPermission([`manage:system`, `manage:api`])')\n            v-divider.my-2\n            v-subheader.pl-4 {{ $t('admin:nav.system') }}\n            v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])')\n              v-list-item-avatar(size='24', tile): v-icon mdi-call-split\n              v-list-item-title {{ $t('admin:api.title') }}\n            v-list-item(to='/mail', color='primary', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-email-multiple-outline\n              v-list-item-title {{ $t('admin:mail.title') }}\n            v-list-item(to='/security', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-lock-check\n              v-list-item-title {{ $t('admin:security.title') }}\n            v-list-item(to='/ssl', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-cloud-lock-outline\n              v-list-item-title {{ $t('admin:ssl.title') }}\n            v-list-item(to='/system', color='primary', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-tune\n              v-list-item-title {{ $t('admin:system.title') }}\n            v-list-item(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')\n              v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline\n              v-list-item-title {{ $t('admin:utilities.title') }}\n            v-list-group(\n              to='/dev'\n              no-action\n              v-if='hasPermission([`manage:system`, `manage:api`])'\n              )\n              v-list-item(slot='activator')\n                v-list-item-avatar(size='24', tile): v-icon mdi-dev-to\n                v-list-item-title {{ $t('admin:dev.title') }}\n\n              v-list-item(to='/dev-flags', color='primary')\n                v-list-item-title {{ $t('admin:dev.flags.title') }}\n              v-list-item(href='/graphql', color='primary')\n                v-list-item-title GraphQL\n              //- v-list-item(to='/dev-graphiql')\n              //-   v-list-item-title {{ $t('admin:dev.graphiql.title') }}\n              //- v-list-item(to='/dev-voyager')\n              //-   v-list-item-title {{ $t('admin:dev.voyager.title') }}\n            v-divider.my-2\n          v-list-item(to='/contribute', color='primary')\n            v-list-item-avatar(size='24', tile): v-icon mdi-heart-outline\n            v-list-item-title {{ $t('admin:contribute.title') }}\n\n    v-main(:class='$vuetify.theme.dark ? \"grey darken-5\" : \"grey lighten-5\"')\n      transition(name='admin-router')\n        router-view\n\n    nav-footer\n    notify\n    search-results\n</template>\n\n<script>\nimport _ from 'lodash'\nimport VueRouter from 'vue-router'\nimport { get, sync } from 'vuex-pathify'\n\nimport statsQuery from 'gql/admin/dashboard/dashboard-query-stats.gql'\n\nimport adminStore from '../store/admin'\n\n/* global WIKI */\n\nWIKI.$store.registerModule('admin', adminStore)\n\nconst router = new VueRouter({\n  mode: 'history',\n  base: '/a',\n  routes: [\n    { path: '/', redirect: '/dashboard' },\n    { path: '/dashboard', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-dashboard.vue') },\n    { path: '/general', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-general.vue') },\n    { path: '/locale', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-locale.vue') },\n    { path: '/navigation', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-navigation.vue') },\n    { path: '/pages', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-pages.vue') },\n    { path: '/pages/:id(\\\\d+)', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-pages-edit.vue') },\n    { path: '/pages/visualize', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-pages-visualize.vue') },\n    { path: '/tags', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-tags.vue') },\n    { path: '/theme', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-theme.vue') },\n    { path: '/groups', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-groups.vue') },\n    { path: '/groups/:id(\\\\d+)', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-groups-edit.vue') },\n    { path: '/users', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-users.vue') },\n    { path: '/users/:id(\\\\d+)', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-users-edit.vue') },\n    { path: '/analytics', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-analytics.vue') },\n    { path: '/auth', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-auth.vue') },\n    { path: '/comments', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-comments.vue') },\n    { path: '/rendering', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-rendering.vue') },\n    { path: '/editor', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-editor.vue') },\n    { path: '/extensions', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-extensions.vue') },\n    { path: '/logging', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-logging.vue') },\n    { path: '/search', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-search.vue') },\n    { path: '/storage', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-storage.vue') },\n    { path: '/api', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-api.vue') },\n    { path: '/mail', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-mail.vue') },\n    { path: '/security', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-security.vue') },\n    { path: '/ssl', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-ssl.vue') },\n    { path: '/system', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-system.vue') },\n    { path: '/utilities', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-utilities.vue') },\n    { path: '/webhooks', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-webhooks.vue') },\n    { path: '/dev-flags', component: () => import(/* webpackChunkName: \"admin-dev\" */ './admin/admin-dev-flags.vue') },\n    { path: '/contribute', component: () => import(/* webpackChunkName: \"admin\" */ './admin/admin-contribute.vue') }\n  ]\n})\n\nexport default {\n  i18nOptions: { namespaces: 'admin' },\n  data() {\n    return {\n      adminDrawerShown: true,\n      scrollStyle: {\n        vuescroll: {},\n        scrollPanel: {\n          initialScrollY: 0,\n          initialScrollX: 0,\n          scrollingX: false,\n          easing: 'easeOutQuad',\n          speed: 1000,\n          verticalNativeBarPos: this.$vuetify.rtl ? `left` : `right`\n        },\n        rail: {\n          gutterOfEnds: '2px'\n        },\n        bar: {\n          onlyShowBarOnScroll: false,\n          background: '#CCC',\n          hoverStyle: {\n            background: '#999'\n          }\n        }\n      }\n    }\n  },\n  computed: {\n    info: sync('admin/info'),\n    permissions: get('user/permissions')\n  },\n  router,\n  created() {\n    this.$store.commit('page/SET_MODE', 'admin')\n  },\n  methods: {\n    hasPermission(prm) {\n      if (_.isArray(prm)) {\n        return _.some(prm, p => {\n          return _.includes(this.permissions, p)\n        })\n      } else {\n        return _.includes(this.permissions, prm)\n      }\n    }\n  },\n  apollo: {\n    info: {\n      query: statsQuery,\n      fetchPolicy: 'network-only',\n      manual: true,\n      result({ data, loading, networkStatus }) {\n        this.info = data.system.info\n      },\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-stats-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.admin {\n  &.theme--light .application--wrap {\n    background-color: lighten(mc('grey', '200'), 2%);\n  }\n}\n\n.admin-router {\n  &-enter-active, &-leave-active {\n    transition: opacity .25s ease;\n    opacity: 1;\n  }\n  &-enter-active {\n    transition-delay: .25s;\n  }\n  &-enter, &-leave-to {\n    opacity: 0;\n  }\n}\n\n.admin-sidebar {\n  .v-list__tile--active {\n    background-color: rgba(mc('theme', 'primary'), .1);\n\n    .v-icon {\n      color: mc('theme', 'primary');\n    }\n  }\n\n  .v-list-group > .v-list-item {\n    padding-left: 0;\n  }\n}\n\n.theme--dark {\n  .admin-sidebar .v-list__tile--active {\n    background-color: rgba(0,0,0, .2);\n    color: mc('blue', '500') !important;\n\n    .v-icon {\n      color: mc('blue', '500');\n    }\n  }\n}\n\n.admin-header {\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n\n  &-title {\n    margin-left: 1rem;\n  }\n}\n\n.admin-providerlogo {\n  width: 250px;\n  height: 50px;\n  float: right;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  margin-left: 16px;\n\n  img {\n    max-width: 100%;\n    max-height: 50px;\n  }\n}\n\n.v-application.admin {\n  code {\n    box-shadow: none;\n    font-family: 'Roboto Mono', monospace;\n    color: mc('pink', '500');\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/comments.vue",
    "content": "<template lang=\"pug\">\n  div(v-intersect.once='onIntersect')\n    v-textarea#discussion-new(\n      outlined\n      flat\n      :placeholder='$t(`common:comments.newPlaceholder`)'\n      auto-grow\n      dense\n      rows='3'\n      hide-details\n      v-model='newcomment'\n      color='blue-grey darken-2'\n      :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'\n      v-if='permissions.write'\n      :aria-label='$t(`common:comments.fieldContent`)'\n    )\n    v-row.mt-2(dense, v-if='!isAuthenticated && permissions.write')\n      v-col(cols='12', lg='6')\n        v-text-field(\n          outlined\n          color='blue-grey darken-2'\n          :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'\n          :placeholder='$t(`common:comments.fieldName`)'\n          hide-details\n          dense\n          autocomplete='name'\n          v-model='guestName'\n          :aria-label='$t(`common:comments.fieldName`)'\n        )\n      v-col(cols='12', lg='6')\n        v-text-field(\n          outlined\n          color='blue-grey darken-2'\n          :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'\n          :placeholder='$t(`common:comments.fieldEmail`)'\n          hide-details\n          type='email'\n          dense\n          autocomplete='email'\n          v-model='guestEmail'\n          :aria-label='$t(`common:comments.fieldEmail`)'\n        )\n    .d-flex.align-center.pt-3(v-if='permissions.write')\n      v-icon.mr-1(color='blue-grey') mdi-language-markdown-outline\n      .caption.blue-grey--text {{$t('common:comments.markdownFormat')}}\n      v-spacer\n      .caption.mr-3(v-if='isAuthenticated')\n        i18next(tag='span', path='common:comments.postingAs')\n          strong(place='name') {{userDisplayName}}\n      v-btn(\n        dark\n        color='blue-grey darken-2'\n        @click='postComment'\n        depressed\n        :aria-label='$t(`common:comments.postComment`)'\n        )\n        v-icon(left) mdi-comment\n        span.text-none {{$t('common:comments.postComment')}}\n    v-divider.mt-3(v-if='permissions.write')\n    .pa-5.d-flex.align-center.justify-center(v-if='isLoading && !hasLoadedOnce')\n      v-progress-circular(\n        indeterminate\n        size='20'\n        width='1'\n        color='blue-grey'\n      )\n      .caption.blue-grey--text.pl-3: em {{$t('common:comments.loading')}}\n    v-timeline(\n      dense\n      v-else-if='comments && comments.length > 0'\n      )\n      v-timeline-item.comments-post(\n        color='pink darken-4'\n        large\n        v-for='cm of comments'\n        :key='`comment-` + cm.id'\n        :id='`comment-post-id-` + cm.id'\n        )\n        template(v-slot:icon)\n          v-avatar(color='blue-grey')\n            //- v-img(src='http://i.pravatar.cc/64')\n            span.white--text.title {{cm.initials}}\n        v-card.elevation-1\n          v-card-text\n            .comments-post-actions(v-if='permissions.manage && !isBusy && commentEditId === 0')\n              v-icon.mr-3(small, @click='editComment(cm)') mdi-pencil\n              v-icon(small, @click='deleteCommentConfirm(cm)') mdi-delete\n            .comments-post-name.caption: strong {{cm.authorName}}\n            .comments-post-date.overline.grey--text {{cm.createdAt | moment('from') }} #[em(v-if='cm.createdAt !== cm.updatedAt') - {{$t('common:comments.modified', { reldate: $options.filters.moment(cm.updatedAt, 'from') })}}]\n            .comments-post-content.mt-3(v-if='commentEditId !== cm.id', v-html='cm.render')\n            .comments-post-editcontent.mt-3(v-else)\n              v-textarea(\n                outlined\n                flat\n                auto-grow\n                dense\n                rows='3'\n                hide-details\n                v-model='commentEditContent'\n                color='blue-grey darken-2'\n                :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'\n              )\n              .d-flex.align-center.pt-3\n                v-spacer\n                v-btn.mr-3(\n                  dark\n                  color='blue-grey darken-2'\n                  @click='editCommentCancel'\n                  outlined\n                  )\n                  v-icon(left) mdi-close\n                  span.text-none {{$t('common:actions.cancel')}}\n                v-btn(\n                  dark\n                  color='blue-grey darken-2'\n                  @click='updateComment'\n                  depressed\n                  )\n                  v-icon(left) mdi-comment\n                  span.text-none {{$t('common:comments.updateComment')}}\n    .pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') {{$t('common:comments.beFirst')}}\n    .text-center.body-2.blue-grey--text(v-else) {{$t('common:comments.none')}}\n\n    v-dialog(v-model='deleteCommentDialogShown', max-width='500')\n      v-card\n        .dialog-header.is-red {{$t('common:comments.deleteConfirmTitle')}}\n        v-card-text.pt-5\n          span {{$t('common:comments.deleteWarn')}}\n          .caption: strong {{$t('common:comments.deletePermanentWarn')}}\n        v-card-chin\n          v-spacer\n          v-btn(text, @click='deleteCommentDialogShown = false') {{$t('common:actions.cancel')}}\n          v-btn(color='red', dark, @click='deleteComment') {{$t('common:actions.delete')}}\n</template>\n\n<script>\nimport gql from 'graphql-tag'\nimport { get } from 'vuex-pathify'\nimport validate from 'validate.js'\nimport _ from 'lodash'\n\nexport default {\n  data () {\n    return {\n      newcomment: '',\n      isLoading: true,\n      hasLoadedOnce: false,\n      comments: [],\n      guestName: '',\n      guestEmail: '',\n      commentToDelete: {},\n      commentEditId: 0,\n      commentEditContent: null,\n      deleteCommentDialogShown: false,\n      isBusy: false,\n      scrollOpts: {\n        duration: 1500,\n        offset: 0,\n        easing: 'easeInOutCubic'\n      }\n    }\n  },\n  computed: {\n    pageId: get('page/id'),\n    permissions: get('page/effectivePermissions@comments'),\n    isAuthenticated: get('user/authenticated'),\n    userDisplayName: get('user/name')\n  },\n  methods: {\n    onIntersect (entries, observer, isIntersecting) {\n      if (isIntersecting) {\n        this.fetch(true)\n      }\n    },\n    async fetch (silent = false) {\n      this.isLoading = true\n      try {\n        const results = await this.$apollo.query({\n          query: gql`\n            query ($locale: String!, $path: String!) {\n              comments {\n                list(locale: $locale, path: $path) {\n                  id\n                  render\n                  authorName\n                  createdAt\n                  updatedAt\n                }\n              }\n            }\n          `,\n          variables: {\n            locale: this.$store.get('page/locale'),\n            path: this.$store.get('page/path')\n          },\n          fetchPolicy: 'network-only'\n        })\n        this.comments = _.get(results, 'data.comments.list', []).map(c => {\n          const nameParts = c.authorName.toUpperCase().split(' ')\n          let initials = _.head(nameParts).charAt(0)\n          if (nameParts.length > 1) {\n            initials += _.last(nameParts).charAt(0)\n          }\n          c.initials = initials\n          return c\n        })\n      } catch (err) {\n        console.warn(err)\n        if (!silent) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: err.message,\n            icon: 'alert'\n          })\n        }\n      }\n      this.isLoading = false\n      this.hasLoadedOnce = true\n    },\n    /**\n     * Post New Comment\n     */\n    async postComment () {\n      let rules = {\n        comment: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2\n          }\n        }\n      }\n      if (!this.isAuthenticated && this.permissions.write) {\n        rules.name = {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255\n          }\n        }\n        rules.email = {\n          presence: {\n            allowEmpty: false\n          },\n          email: true\n        }\n      }\n      const validationResults = validate({\n        comment: this.newcomment,\n        name: this.guestName,\n        email: this.guestEmail\n      }, rules, { format: 'flat' })\n\n      if (validationResults) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: validationResults[0],\n          icon: 'alert'\n        })\n        return\n      }\n\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $pageId: Int!\n              $replyTo: Int\n              $content: String!\n              $guestName: String\n              $guestEmail: String\n            ) {\n              comments {\n                create (\n                  pageId: $pageId\n                  replyTo: $replyTo\n                  content: $content\n                  guestName: $guestName\n                  guestEmail: $guestEmail\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                  id\n                }\n              }\n            }\n          `,\n          variables: {\n            pageId: this.pageId,\n            replyTo: 0,\n            content: this.newcomment,\n            guestName: this.guestName,\n            guestEmail: this.guestEmail\n          }\n        })\n\n        if (_.get(resp, 'data.comments.create.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('common:comments.postSuccess'),\n            icon: 'check'\n          })\n\n          this.newcomment = ''\n          await this.fetch()\n          this.$nextTick(() => {\n            this.$vuetify.goTo(`#comment-post-id-${_.get(resp, 'data.comments.create.id', 0)}`, this.scrollOpts)\n          })\n        } else {\n          throw new Error(_.get(resp, 'data.comments.create.responseResult.message', 'An unexpected error occurred.'))\n        }\n      } catch (err) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n      }\n    },\n    /**\n     * Show Comment Editing Form\n     */\n    async editComment (cm) {\n      this.$store.commit(`loadingStart`, 'comments-edit')\n      this.isBusy = true\n      try {\n        const results = await this.$apollo.query({\n          query: gql`\n            query ($id: Int!) {\n              comments {\n                single(id: $id) {\n                  content\n                }\n              }\n            }\n          `,\n          variables: {\n            id: cm.id\n          },\n          fetchPolicy: 'network-only'\n        })\n        this.commentEditContent = _.get(results, 'data.comments.single.content', null)\n        if (this.commentEditContent === null) {\n          throw new Error('Failed to load comment content.')\n        }\n      } catch (err) {\n        console.warn(err)\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n      }\n      this.commentEditId = cm.id\n      this.isBusy = false\n      this.$store.commit(`loadingStop`, 'comments-edit')\n    },\n    /**\n     * Cancel Comment Edit\n     */\n    editCommentCancel () {\n      this.commentEditId = 0\n      this.commentEditContent = null\n    },\n    /**\n     * Update Comment with new content\n     */\n    async updateComment () {\n      this.$store.commit(`loadingStart`, 'comments-edit')\n      this.isBusy = true\n      try {\n        if (this.commentEditContent.length < 2) {\n          throw new Error(this.$t('common:comments.contentMissingError'))\n        }\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $id: Int!\n              $content: String!\n            ) {\n              comments {\n                update (\n                  id: $id,\n                  content: $content\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                  render\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.commentEditId,\n            content: this.commentEditContent\n          }\n        })\n\n        if (_.get(resp, 'data.comments.update.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('common:comments.updateSuccess'),\n            icon: 'check'\n          })\n\n          const cm = _.find(this.comments, ['id', this.commentEditId])\n          cm.render = _.get(resp, 'data.comments.update.render', '-- Failed to load updated comment --')\n          cm.updatedAt = (new Date()).toISOString()\n\n          this.editCommentCancel()\n        } else {\n          throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occurred.'))\n        }\n      } catch (err) {\n        console.warn(err)\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n      }\n      this.isBusy = false\n      this.$store.commit(`loadingStop`, 'comments-edit')\n    },\n    /**\n     * Show Delete Comment Confirmation Dialog\n     */\n    deleteCommentConfirm (cm) {\n      this.commentToDelete = cm\n      this.deleteCommentDialogShown = true\n    },\n    /**\n     * Delete Comment\n     */\n    async deleteComment () {\n      this.$store.commit(`loadingStart`, 'comments-delete')\n      this.isBusy = true\n      this.deleteCommentDialogShown = false\n\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $id: Int!\n            ) {\n              comments {\n                delete (\n                  id: $id\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            id: this.commentToDelete.id\n          }\n        })\n\n        if (_.get(resp, 'data.comments.delete.responseResult.succeeded', false)) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('common:comments.deleteSuccess'),\n            icon: 'check'\n          })\n\n          this.comments = _.reject(this.comments, ['id', this.commentToDelete.id])\n        } else {\n          throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occurred.'))\n        }\n      } catch (err) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n      }\n      this.isBusy = false\n      this.$store.commit(`loadingStop`, 'comments-delete')\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.comments-post {\n  position: relative;\n\n  &:hover {\n    .comments-post-actions {\n      opacity: 1;\n    }\n  }\n\n  &-actions {\n    position: absolute;\n    top: 16px;\n    right: 16px;\n    opacity: 0;\n    transition: opacity .4s ease;\n  }\n\n  &-content {\n    > p:first-child {\n      padding-top: 0;\n    }\n\n    p {\n      padding-top: 1rem;\n      margin-bottom: 0;\n    }\n\n    img {\n      max-width: 100%;\n      border-radius: 5px;\n    }\n\n    code {\n      background-color: rgba(mc('pink', '500'), .1);\n      box-shadow: none;\n    }\n\n    pre > code {\n      margin-top: 1rem;\n      padding: 12px;\n      background-color: #111;\n      box-shadow: none;\n      border-radius: 5px;\n      width: 100%;\n      color: #FFF;\n      font-weight: 400;\n      font-size: .85rem;\n      font-family: Roboto Mono, monospace;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/common/duration-picker.vue",
    "content": "<template lang='pug'>\n  v-toolbar.radius-7(flat, :color='$vuetify.theme.dark ? \"grey darken-4-l3\" : \"grey lighten-3\"')\n    .body-2.mr-3 {{$t('common:duration.every')}}\n    v-text-field(\n      solo\n      hide-details\n      flat\n      reverse\n      v-model='minutes'\n      style='flex: 1 1 70px;'\n    )\n    .body-2.mx-3 {{$t('common:duration.minutes')}}\n    v-divider.mr-3\n    v-text-field(\n      solo\n      hide-details\n      flat\n      reverse\n      v-model='hours'\n      style='flex: 1 1 70px;'\n    )\n    .body-2.mx-3 {{$t('common:duration.hours')}}\n    v-divider.mr-3\n    v-text-field(\n      solo\n      hide-details\n      flat\n      reverse\n      v-model='days'\n      style='flex: 1 1 70px;'\n    )\n    .body-2.mx-3 {{$t('common:duration.days')}}\n    v-divider.mr-3\n    v-text-field(\n      solo\n      hide-details\n      flat\n      reverse\n      v-model='months'\n      style='flex: 1 1 70px;'\n    )\n    .body-2.mx-3 {{$t('common:duration.months')}}\n    v-divider.mr-3\n    v-text-field(\n      solo\n      hide-details\n      flat\n      reverse\n      v-model='years'\n      style='flex: 1 1 70px;'\n    )\n    .body-2.mx-3 {{$t('common:duration.years')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport moment from 'moment'\n\nexport default {\n  props: {\n    value: {\n      type: String,\n      default: 'PT5M'\n    }\n  },\n  data() {\n    return {\n      duration: moment.duration(0)\n    }\n  },\n  computed: {\n    years: {\n      get() { return this.duration.years() || 0 },\n      set(val) { this.rebuild(_.toNumber(val), 'years') }\n    },\n    months: {\n      get() { return this.duration.months() || 0 },\n      set(val) { this.rebuild(_.toNumber(val), 'months') }\n    },\n    days: {\n      get() { return this.duration.days() || 0 },\n      set(val) { this.rebuild(_.toNumber(val), 'days') }\n    },\n    hours: {\n      get() { return this.duration.hours() || 0 },\n      set(val) { this.rebuild(_.toNumber(val), 'hours') }\n    },\n    minutes: {\n      get() { return this.duration.minutes() || 0 },\n      set(val) { this.rebuild(_.toNumber(val), 'minutes') }\n    }\n  },\n  watch: {\n    value(newValue, oldValue) {\n      this.duration = moment.duration(newValue)\n    }\n  },\n  methods: {\n    rebuild(val, unit) {\n      if (!_.isFinite(val) || val < 0) {\n        val = 0\n      }\n      const newDuration = {\n        minutes: this.duration.minutes(),\n        hours: this.duration.hours(),\n        days: this.duration.days(),\n        months: this.duration.months(),\n        years: this.duration.years()\n      }\n      _.set(newDuration, unit, val)\n      this.duration = moment.duration(newDuration)\n      this.$emit('input', this.duration.toISOString())\n    }\n  },\n  mounted() {\n    this.duration = moment.duration(this.value)\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/common/loader.vue",
    "content": "<template lang='pug'>\n  v-dialog(v-model='value', persistent, max-width='350', :overlay-color='color', overlay-opacity='.7')\n    v-card.loader-dialog.radius-7(:color='color', dark)\n      v-card-text.text-center.py-4\n        atom-spinner.is-inline(\n          v-if='mode === `loading`'\n          :animation-duration='1000'\n          :size='60'\n          color='#FFF'\n          )\n        img(v-else-if='mode === `icon`', :src='`/_assets/svg/icon-` + icon + `.svg`', :alt='icon')\n        .subtitle-1.white--text {{ title }}\n        .caption {{ subtitle }}\n</template>\n\n<script>\nimport { AtomSpinner } from 'epic-spinners'\n\nexport default {\n  components: {\n    AtomSpinner\n  },\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    },\n    color: {\n      type: String,\n      default: 'blue darken-3'\n    },\n    title: {\n      type: String,\n      default: 'Working...'\n    },\n    subtitle: {\n      type: String,\n      default: 'Please wait'\n    },\n    mode: {\n      type: String,\n      default: 'loading'\n    },\n    icon: {\n      type: String,\n      default: 'checkmark'\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n  .loader-dialog {\n    transition: all .4s ease;\n\n    .atom-spinner.is-inline {\n      display: inline-block;\n    }\n    .caption {\n      color: rgba(255,255,255,.7);\n    }\n\n    img {\n      width: 80px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "client/components/common/nav-header.vue",
    "content": "<template lang='pug'>\n  v-app-bar.nav-header(color='black', dark, app, :clipped-left='!$vuetify.rtl', :clipped-right='$vuetify.rtl', fixed, flat, :extended='searchIsShown && $vuetify.breakpoint.smAndDown')\n    v-toolbar(color='deep-purple', flat, slot='extension', v-if='searchIsShown && $vuetify.breakpoint.smAndDown')\n      v-text-field(\n        ref='searchFieldMobile'\n        v-model='search'\n        clearable\n        background-color='deep-purple'\n        color='white'\n        :label='$t(`common:header.search`)'\n        single-line\n        solo\n        flat\n        hide-details\n        prepend-inner-icon='mdi-magnify'\n        :loading='searchIsLoading'\n        @keyup.enter='searchEnter'\n        autocomplete='off'\n      )\n    v-layout(row)\n      v-flex(xs5, md4)\n        v-toolbar.nav-header-inner(color='black', dark, flat, :class='$vuetify.rtl ? `pr-3` : `pl-3`')\n          v-avatar(tile, size='34', @click='goHome')\n            v-img.org-logo(:src='logoUrl')\n          //- v-menu(open-on-hover, offset-y, bottom, left, min-width='250', transition='slide-y-transition')\n          //-   template(v-slot:activator='{ on }')\n          //-     v-app-bar-nav-icon.btn-animate-app(v-on='on', :class='$vuetify.rtl ? `mx-0` : ``')\n          //-       v-icon mdi-menu\n          //-   v-list(nav, :light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark', :class='$vuetify.theme.dark ? `grey darken-4` : ``')\n          //-     v-list-item.pl-4(href='/')\n          //-       v-list-item-avatar(size='24'): v-icon(color='blue') mdi-home\n          //-       v-list-item-title.body-2 {{$t('common:header.home')}}\n          //-     v-list-item.pl-4(@click='')\n          //-       v-list-item-avatar(size='24'): v-icon(color='grey lighten-2') mdi-file-tree\n          //-       v-list-item-content\n          //-         v-list-item-title.body-2.grey--text.text--ligten-2 {{$t('common:header.siteMap')}}\n          //-         v-list-item-subtitle.overline.grey--text.text--lighten-2 Coming soon\n          //-     v-list-item.pl-4(href='/t')\n          //-       v-list-item-avatar(size='24'): v-icon(color='teal') mdi-tag-multiple\n          //-       v-list-item-title.body-2 {{$t('common:header.browseTags')}}\n          //-     v-list-item.pl-4(@click='assets')\n          //-       v-list-item-avatar(size='24'): v-icon(color='grey lighten-2') mdi-folder-multiple-image\n          //-       v-list-item-content\n          //-         v-list-item-title.body-2.grey--text.text--ligten-2 {{$t('common:header.imagesFiles')}}\n          //-         v-list-item-subtitle.overline.grey--text.text--lighten-2 Coming soon\n          v-toolbar-title(:class='{ \"mx-3\": $vuetify.breakpoint.mdAndUp, \"mx-1\": $vuetify.breakpoint.smAndDown }')\n            span.subheading {{title}}\n      v-flex(md4, v-if='$vuetify.breakpoint.mdAndUp')\n        v-toolbar.nav-header-inner(color='black', dark, flat)\n          slot(name='mid')\n            transition(name='navHeaderSearch', v-if='searchIsShown')\n              v-text-field(\n                ref='searchField',\n                v-if='searchIsShown && $vuetify.breakpoint.mdAndUp',\n                v-model='search',\n                color='white',\n                :label='$t(`common:header.search`)',\n                single-line,\n                solo\n                flat\n                rounded\n                hide-details,\n                prepend-inner-icon='mdi-magnify',\n                :loading='searchIsLoading',\n                @keyup.enter='searchEnter'\n                @keyup.esc='searchClose'\n                @focus='searchFocus'\n                @blur='searchBlur'\n                @keyup.down='searchMove(`down`)'\n                @keyup.up='searchMove(`up`)'\n                autocomplete='off'\n              )\n            v-tooltip(bottom)\n              template(v-slot:activator='{ on }')\n                v-btn.ml-2.mr-0(icon, v-on='on', href='/t', :aria-label='$t(`common:header.browseTags`)')\n                  v-icon(color='grey') mdi-tag-multiple\n              span {{$t('common:header.browseTags')}}\n      v-flex(xs7, md4)\n        v-toolbar.nav-header-inner.pr-4(color='black', dark, flat)\n          v-spacer\n          .navHeaderLoading.mr-3\n            v-progress-circular(indeterminate, color='blue', :size='22', :width='2' v-show='isLoading')\n\n          slot(name='actions')\n\n          //- (mobile) SEARCH TOGGLE\n\n          v-btn(\n            v-if='!hideSearch && $vuetify.breakpoint.smAndDown'\n            @click='searchToggle'\n            icon\n            )\n            v-icon(color='grey') mdi-magnify\n\n          //- LANGUAGES\n\n          template(v-if='mode === `view` && locales.length > 0')\n            v-menu(offset-y, bottom, transition='slide-y-transition', max-height='320px', min-width='210px', left)\n              template(v-slot:activator='{ on: menu, attrs }')\n                v-tooltip(bottom)\n                  template(v-slot:activator='{ on: tooltip }')\n                    v-btn(\n                      icon\n                      v-bind='attrs'\n                      v-on='{ ...menu, ...tooltip }'\n                      :class='$vuetify.rtl ? `ml-3` : ``'\n                      tile\n                      height='64'\n                      :aria-label='$t(`common:header.language`)'\n                      )\n                      v-icon(color='grey') mdi-web\n                  span {{$t('common:header.language')}}\n              v-list(nav)\n                template(v-for='(lc, idx) of locales')\n                  v-list-item(@click='changeLocale(lc)')\n                    v-list-item-action(style='min-width:auto;'): v-chip(:color='lc.code === locale ? `blue` : `grey`', small, label, dark) {{lc.code.toUpperCase()}}\n                    v-list-item-title {{lc.name}}\n            v-divider(vertical)\n\n          //- PAGE ACTIONS\n\n          template(v-if='hasAnyPagePermissions && path && mode !== `edit`')\n            v-menu(offset-y, bottom, transition='slide-y-transition', left)\n              template(v-slot:activator='{ on: menu, attrs }')\n                v-tooltip(bottom)\n                  template(v-slot:activator='{ on: tooltip }')\n                    v-btn(\n                      icon\n                      v-bind='attrs'\n                      v-on='{ ...menu, ...tooltip }'\n                      :class='$vuetify.rtl ? `ml-3` : ``'\n                      tile\n                      height='64'\n                      :aria-label='$t(`common:header.pageActions`)'\n                      )\n                      v-icon(color='grey') mdi-file-document-edit-outline\n                  span {{$t('common:header.pageActions')}}\n              v-list(nav, :light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark', :class='$vuetify.theme.dark ? `grey darken-4` : ``')\n                .overline.pa-4.grey--text {{$t('common:header.currentPage')}}\n                v-list-item.pl-4(@click='pageView', v-if='mode !== `view`')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-file-document-outline\n                  v-list-item-title.body-2 {{$t('common:header.view')}}\n                v-list-item.pl-4(@click='pageEdit', v-if='mode !== `edit` && hasWritePagesPermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-file-document-edit-outline\n                  v-list-item-title.body-2 {{$t('common:header.edit')}}\n                v-list-item.pl-4(@click='pageHistory', v-if='mode !== `history` && hasReadHistoryPermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-history\n                  v-list-item-content\n                    v-list-item-title.body-2 {{$t('common:header.history')}}\n                v-list-item.pl-4(@click='pageSource', v-if='mode !== `source` && hasReadSourcePermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-code-tags\n                  v-list-item-title.body-2 {{$t('common:header.viewSource')}}\n                v-list-item.pl-4(@click='pageConvert', v-if='hasWritePagesPermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-lightning-bolt\n                  v-list-item-title.body-2 {{$t('common:header.convert')}}\n                v-list-item.pl-4(@click='pageDuplicate', v-if='hasWritePagesPermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-content-duplicate\n                  v-list-item-title.body-2 {{$t('common:header.duplicate')}}\n                v-list-item.pl-4(@click='pageMove', v-if='hasManagePagesPermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='indigo') mdi-content-save-move-outline\n                  v-list-item-content\n                    v-list-item-title.body-2 {{$t('common:header.move')}}\n                v-list-item.pl-4(@click='pageDelete', v-if='hasDeletePagesPermission')\n                  v-list-item-avatar(size='24', tile): v-icon(color='red darken-2') mdi-trash-can-outline\n                  v-list-item-title.body-2 {{$t('common:header.delete')}}\n            v-divider(vertical)\n\n          //- NEW PAGE\n\n          template(v-if='hasNewPagePermission && path && mode !== `edit`')\n            v-tooltip(bottom)\n              template(v-slot:activator='{ on }')\n                v-btn(icon, tile, height='64', v-on='on', @click='pageNew', :aria-label='$t(`common:header.newPage`)')\n                  v-icon(color='grey') mdi-text-box-plus-outline\n              span {{$t('common:header.newPage')}}\n            v-divider(vertical)\n\n          //- ADMIN\n\n          template(v-if='isAuthenticated && isAdmin')\n            v-tooltip(bottom, v-if='mode !== `admin`')\n              template(v-slot:activator='{ on }')\n                v-btn(icon, tile, height='64', v-on='on', href='/a', :aria-label='$t(`common:header.admin`)')\n                  v-icon(color='grey') mdi-cog\n              span {{$t('common:header.admin')}}\n            v-btn(v-else, text, tile, height='64', href='/', :aria-label='$t(`common:actions.exit`)')\n              v-icon(left, color='grey') mdi-exit-to-app\n              span {{$t('common:actions.exit')}}\n            v-divider(vertical)\n\n          //- ACCOUNT\n\n          v-menu(v-if='isAuthenticated', offset-y, bottom, min-width='300', transition='slide-y-transition', left)\n            template(v-slot:activator='{ on: menu, attrs }')\n              v-tooltip(bottom)\n                template(v-slot:activator='{ on: tooltip }')\n                  v-btn(\n                    icon\n                    v-bind='attrs'\n                    v-on='{ ...menu, ...tooltip }'\n                    :class='$vuetify.rtl ? `ml-0` : ``'\n                    tile\n                    height='64'\n                    :aria-label='$t(`common:header.account`)'\n                    )\n                    v-icon(v-if='picture.kind === `initials`', color='grey') mdi-account-circle\n                    v-avatar(v-else-if='picture.kind === `image`', :size='34')\n                      v-img(:src='picture.url')\n                span {{$t('common:header.account')}}\n            v-list(nav)\n              v-list-item.py-3.grey(:class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-5`')\n                v-list-item-avatar\n                  v-avatar.blue(v-if='picture.kind === `initials`', :size='40')\n                    span.white--text.subheading {{picture.initials}}\n                  v-avatar(v-else-if='picture.kind === `image`', :size='40')\n                    v-img(:src='picture.url')\n                v-list-item-content\n                  v-list-item-title {{name}}\n                  v-list-item-subtitle {{email}}\n              //- v-list-item(href='/w', disabled)\n              //-   v-list-item-action: v-icon(color='blue') mdi-view-compact-outline\n              //-   v-list-item-content\n              //-     v-list-item-title {{$t('common:header.myWiki')}}\n              //-     v-list-item-subtitle.overline Coming soon\n              v-list-item(href='/p')\n                v-list-item-action: v-icon(color='blue-grey') mdi-face-profile\n                v-list-item-content\n                  v-list-item-title(:class='$vuetify.theme.dark ? `blue-grey--text text--lighten-3` : `blue-grey--text`') {{$t('common:header.profile')}}\n              v-list-item(@click='logout')\n                v-list-item-action: v-icon(color='red') mdi-logout\n                v-list-item-title.red--text {{$t('common:header.logout')}}\n\n          v-tooltip(v-else, left)\n            template(v-slot:activator='{ on }')\n              v-btn(icon, v-on='on', color='grey darken-3', href='/login', :aria-label='$t(`common:header.login`)')\n                v-icon(color='grey') mdi-account-circle\n            span {{$t('common:header.login')}}\n\n    page-selector(mode='create', v-model='newPageModal', :open-handler='pageNewCreate', :locale='locale')\n    page-selector(mode='move', v-model='movePageModal', :open-handler='pageMoveRename', :path='path', :locale='locale')\n    page-selector(mode='create', v-model='duplicateOpts.modal', :open-handler='pageDuplicateHandle', :path='duplicateOpts.path', :locale='duplicateOpts.locale')\n    page-delete(v-model='deletePageModal', v-if='path && path.length')\n    page-convert(v-model='convertPageModal', v-if='path && path.length')\n\n    .nav-header-dev(v-if='isDevMode')\n      v-icon mdi-alert\n      div\n        .overline DEVELOPMENT VERSION\n        .overline This code base is NOT for production use!\n</template>\n\n<script>\nimport { get, sync } from 'vuex-pathify'\nimport _ from 'lodash'\n\nimport movePageMutation from 'gql/common/common-pages-mutation-move.gql'\n\n/* global siteConfig, siteLangs */\n\nexport default {\n  components: {\n    PageDelete: () => import('./page-delete.vue'),\n    PageConvert: () => import('./page-convert.vue')\n  },\n  props: {\n    dense: {\n      type: Boolean,\n      default: false\n    },\n    hideSearch: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      menuIsShown: true,\n      searchIsShown: true,\n      searchAdvMenuShown: false,\n      newPageModal: false,\n      movePageModal: false,\n      convertPageModal: false,\n      deletePageModal: false,\n      locales: siteLangs,\n      isDevMode: false,\n      duplicateOpts: {\n        locale: 'en',\n        path: 'new-page',\n        modal: false\n      }\n    }\n  },\n  computed: {\n    search: sync('site/search'),\n    searchIsFocused: sync('site/searchIsFocused'),\n    searchIsLoading: sync('site/searchIsLoading'),\n    searchRestrictLocale: sync('site/searchRestrictLocale'),\n    searchRestrictPath: sync('site/searchRestrictPath'),\n    isLoading: get('isLoading'),\n    title: get('site/title'),\n    logoUrl: get('site/logoUrl'),\n    path: get('page/path'),\n    locale: get('page/locale'),\n    mode: get('page/mode'),\n    name: get('user/name'),\n    email: get('user/email'),\n    pictureUrl: get('user/pictureUrl'),\n    isAuthenticated: get('user/authenticated'),\n    permissions: get('user/permissions'),\n    picture () {\n      if (this.pictureUrl && this.pictureUrl.length > 1) {\n        return {\n          kind: 'image',\n          url: (this.pictureUrl === 'internal') ? `/_userav/${this.$store.get('user/id')}` : this.pictureUrl\n        }\n      } else {\n        const nameParts = this.name.toUpperCase().split(' ')\n        let initials = _.head(nameParts).charAt(0)\n        if (nameParts.length > 1) {\n          initials += _.last(nameParts).charAt(0)\n        }\n        return {\n          kind: 'initials',\n          initials\n        }\n      }\n    },\n    isAdmin () {\n      return _.intersection(this.permissions, ['manage:system', 'write:users', 'manage:users', 'write:groups', 'manage:groups', 'manage:navigation', 'manage:theme', 'manage:api']).length > 0\n    },\n    hasNewPagePermission () {\n      return this.hasAdminPermission || _.intersection(this.permissions, ['write:pages']).length > 0\n    },\n    hasAdminPermission: get('page/effectivePermissions@system.manage'),\n    hasWritePagesPermission: get('page/effectivePermissions@pages.write'),\n    hasManagePagesPermission: get('page/effectivePermissions@pages.manage'),\n    hasDeletePagesPermission: get('page/effectivePermissions@pages.delete'),\n    hasReadSourcePermission: get('page/effectivePermissions@source.read'),\n    hasReadHistoryPermission: get('page/effectivePermissions@history.read'),\n    hasAnyPagePermissions () {\n      return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission ||\n        this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission\n    }\n  },\n  created () {\n    if (this.hideSearch || this.dense || this.$vuetify.breakpoint.smAndDown) {\n      this.searchIsShown = false\n    }\n  },\n  mounted () {\n    this.$root.$on('pageEdit', () => {\n      this.pageEdit()\n    })\n    this.$root.$on('pageHistory', () => {\n      this.pageHistory()\n    })\n    this.$root.$on('pageSource', () => {\n      this.pageSource()\n    })\n    this.$root.$on('pageMove', () => {\n      this.pageMove()\n    })\n    this.$root.$on('pageConvert', () => {\n      this.pageConvert()\n    })\n    this.$root.$on('pageDuplicate', () => {\n      this.pageDuplicate()\n    })\n    this.$root.$on('pageDelete', () => {\n      this.pageDelete()\n    })\n    this.isDevMode = siteConfig.devMode === true\n  },\n  methods: {\n    searchFocus () {\n      this.searchIsFocused = true\n    },\n    searchBlur () {\n      this.searchIsFocused = false\n    },\n    searchClose () {\n      this.search = ''\n      this.searchBlur()\n    },\n    searchToggle () {\n      this.searchIsShown = !this.searchIsShown\n      if (this.searchIsShown) {\n        _.delay(() => {\n          this.$refs.searchFieldMobile.focus()\n        }, 200)\n      }\n    },\n    searchEnter () {\n      this.$root.$emit('searchEnter', true)\n    },\n    searchMove(dir) {\n      this.$root.$emit('searchMove', dir)\n    },\n    pageNew () {\n      this.newPageModal = true\n    },\n    pageNewCreate ({ path, locale }) {\n      window.location.assign(`/e/${locale}/${path}`)\n    },\n    pageView () {\n      window.location.assign(`/${this.locale}/${this.path}`)\n    },\n    pageEdit () {\n      window.location.assign(`/e/${this.locale}/${this.path}`)\n    },\n    pageHistory () {\n      window.location.assign(`/h/${this.locale}/${this.path}`)\n    },\n    pageSource () {\n      window.location.assign(`/s/${this.locale}/${this.path}`)\n    },\n    pageDuplicate () {\n      const pathParts = this.path.split('/')\n      this.duplicateOpts = {\n        locale: this.locale,\n        path: (pathParts.length > 1) ? _.initial(pathParts).join('/') + `/new-page` : `new-page`,\n        modal: true\n      }\n    },\n    pageDuplicateHandle ({ locale, path }) {\n      window.location.assign(`/e/${locale}/${path}?from=${this.$store.get('page/id')}`)\n    },\n    pageConvert () {\n      this.convertPageModal = true\n    },\n    pageMove () {\n      this.movePageModal = true\n    },\n    async pageMoveRename ({ path, locale }) {\n      this.$store.commit(`loadingStart`, 'page-move')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: movePageMutation,\n          variables: {\n            id: this.$store.get('page/id'),\n            destinationLocale: locale,\n            destinationPath: path\n          }\n        })\n        if (_.get(resp, 'data.pages.move.responseResult.succeeded', false)) {\n          window.location.replace(`/${locale}/${path}`)\n        } else {\n          throw new Error(_.get(resp, 'data.pages.move.responseResult.message', this.$t('common:error.unexpected')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n        this.$store.commit(`loadingStop`, 'page-move')\n      }\n    },\n    pageDelete () {\n      this.deletePageModal = true\n    },\n    assets () {\n      // window.location.assign(`/f`)\n      this.$store.commit('showNotification', {\n        style: 'indigo',\n        message: `Coming soon...`,\n        icon: 'ferry'\n      })\n    },\n    async changeLocale (locale) {\n      await this.$i18n.i18next.changeLanguage(locale.code)\n      switch (this.mode) {\n        case 'view':\n        case 'history':\n          window.location.assign(`/${locale.code}/${this.path}`)\n          break\n      }\n    },\n    logout () {\n      window.location.assign('/logout')\n    },\n    goHome () {\n      if (this.locales && this.locales.length > 0) {\n        window.location.assign(`/${this.locale}/home`)\n      } else {\n        window.location.assign('/')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.nav-header {\n  //z-index: 1000;\n\n  .v-toolbar__extension {\n    padding: 0;\n\n    .v-toolbar__content {\n      padding: 0;\n    }\n    .v-text-field .v-input__prepend-inner {\n      padding: 0 14px 0 5px;\n      padding-right: 14px;\n    }\n  }\n\n  .org-logo {\n    cursor: pointer;\n  }\n\n  &-inner {\n    .v-toolbar__content {\n      padding: 0;\n    }\n  }\n\n  &-search-adv {\n    position: absolute;\n    top: 7px;\n    right: 12px;\n    border-radius: 4px !important;\n\n    @at-root .v-application--is-rtl & {\n      right: initial;\n      left: 12px;\n    }\n\n    &::before {\n      border-radius: 4px !important;\n    }\n\n    &:hover, &:focus {\n      position: absolute !important;\n\n      &::before {\n        border-radius: 4px;\n      }\n    }\n  }\n\n  &-dev {\n    background-color: mc('red', '600');\n    position: absolute;\n    top: 11px;\n    left: 255px;\n    padding: 5px 15px;\n    border-radius: 5px;\n    display: flex;\n\n    .v-icon {\n      margin-right: 15px;\n    }\n\n    .overline:nth-child(2) {\n      text-transform: none;\n    }\n  }\n}\n\n.navHeaderSearch {\n  &-enter-active, &-leave-active {\n    transition: opacity .25s ease, transform .25s ease;\n    opacity: 1;\n  }\n  &-enter-active {\n    transition-delay: .25s;\n  }\n  &-enter, &-leave-to {\n    opacity: 0;\n    transform: scale(.7, .7);\n  }\n}\n.navHeaderLoading { // To avoid search bar jumping\n  width: 22px;\n}\n\n</style>\n"
  },
  {
    "path": "client/components/common/notify.vue",
    "content": "<template lang='pug'>\n  v-snackbar.nav-notify(\n    :color='notification.style'\n    top\n    multi-line\n    v-model='notificationState'\n    :timeout='6000'\n    )\n    .text-left\n      v-icon.mr-3(dark) mdi-{{ notification.icon }}\n      span {{ notification.message }}\n</template>\n\n<script>\nimport { get, sync } from 'vuex-pathify'\n\nexport default {\n  data() {\n    return { }\n  },\n  computed: {\n    notification: get('notification'),\n    notificationState: sync('notification@isActive')\n  }\n}\n</script>\n\n<style lang='scss'>\n.nav-notify {\n  top: -64px;\n  padding-top: 0;\n  z-index: 999;\n\n  .v-snack__wrapper {\n    border-top-left-radius: 0;\n    border-top-right-radius: 0;\n    position: relative;\n    margin-top: 0;\n\n    &::after {\n      content: '';\n      display: block;\n      width: 100%;\n      height: 2px;\n      background-color: rgba(255,255,255,.4);\n      position: absolute;\n      bottom: 0;\n      left: 0;\n      animation: nav-notify-anim 6s linear;\n    }\n  }\n}\n\n@keyframes nav-notify-anim {\n  0% {\n    width: 100%;\n  }\n  100% {\n    width: 0%;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/common/page-convert.vue",
    "content": "<template lang='pug'>\n  v-dialog(\n    v-model='isShown'\n    max-width='550'\n    persistent\n    overlay-color='blue-grey darken-4'\n    overlay-opacity='.7'\n    )\n    v-card\n      .dialog-header.is-short.is-dark\n        v-icon.mr-2(color='white') mdi-lightning-bolt\n        span {{$t('common:page.convert')}}\n      v-card-text.pt-5\n        i18next.body-2(path='common:page.convertTitle', tag='div')\n          span.blue-grey--text.text--darken-2(place='title') {{pageTitle}}\n        v-select.mt-5(\n          :items=`[\n            { value: 'markdown', text: 'Markdown' },\n            { value: 'ckeditor', text: 'Visual Editor' },\n            { value: 'code', text: 'Raw HTML' }\n          ]`\n          outlined\n          dense\n          hide-details\n          v-model='newEditor'\n        )\n        .caption.mt-5 {{$t('common:page.convertSubtitle')}}\n      v-card-chin\n        v-spacer\n        v-btn(text, @click='discard', :disabled='loading') {{$t('common:actions.cancel')}}\n        v-btn.px-4(color='grey darken-3', @click='convertPage', :loading='loading').white--text {{$t('common:actions.convert')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get } from 'vuex-pathify'\nimport gql from 'graphql-tag'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      loading: false,\n      newEditor: ''\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    pageTitle: get('page/title'),\n    pagePath: get('page/path'),\n    pageLocale: get('page/locale'),\n    pageId: get('page/id'),\n    pageEditor: get('page/editor')\n  },\n  mounted () {\n    this.newEditor = this.pageEditor\n  },\n  methods: {\n    discard() {\n      this.isShown = false\n    },\n    async convertPage() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'page-convert')\n      this.$nextTick(async () => {\n        try {\n          const resp = await this.$apollo.mutate({\n            mutation: gql`\n              mutation (\n                $id: Int!\n                $editor: String!\n                ) {\n                  pages {\n                    convert(\n                      id: $id\n                      editor: $editor\n                    ) {\n                      responseResult {\n                        succeeded\n                        errorCode\n                        slug\n                        message\n                      }\n                    }\n                  }\n              }\n            `,\n            variables: {\n              id: this.pageId,\n              editor: this.newEditor\n            }\n          })\n          if (_.get(resp, 'data.pages.convert.responseResult.succeeded', false)) {\n            this.isShown = false\n            window.location.assign(`/e/${this.pageLocale}/${this.pagePath}`)\n          } else {\n            throw new Error(_.get(resp, 'data.pages.convert.responseResult.message', this.$t('common:error.unexpected')))\n          }\n        } catch (err) {\n          this.$store.commit('pushGraphError', err)\n        }\n        this.$store.commit(`loadingStop`, 'page-convert')\n        this.loading = false\n      })\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/common/page-delete.vue",
    "content": "<template lang='pug'>\n  v-dialog(\n    v-model='isShown'\n    max-width='550'\n    persistent\n    overlay-color='red darken-4'\n    overlay-opacity='.7'\n    )\n    v-card\n      .dialog-header.is-short.is-red\n        v-icon.mr-2(color='white') mdi-file-document-box-remove-outline\n        span {{$t('common:page.delete')}}\n      v-card-text.pt-5\n        i18next.body-1(path='common:page.deleteTitle', tag='div')\n          span.red--text.text--darken-2(place='title') {{pageTitle}}\n        .caption {{$t('common:page.deleteSubtitle')}}\n        v-chip.mt-3.ml-0.mr-1(label, color='red lighten-4', small)\n          .caption.red--text.text--darken-2 {{pageLocale.toUpperCase()}}\n        v-chip.mt-3.mx-0(label, color='red lighten-5', small)\n          span.red--text.text--darken-2 /{{pagePath}}\n      v-card-chin\n        v-spacer\n        v-btn(text, @click='discard', :disabled='loading') {{$t('common:actions.cancel')}}\n        v-btn.px-4(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get } from 'vuex-pathify'\n\nimport deletePageMutation from 'gql/common/common-pages-mutation-delete.gql'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      loading: false\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    pageTitle: get('page/title'),\n    pagePath: get('page/path'),\n    pageLocale: get('page/locale'),\n    pageId: get('page/id')\n  },\n  watch: {\n    isShown(newValue, oldValue) {\n      if (newValue) {\n        document.body.classList.add('page-deleted-pending')\n      }\n    }\n  },\n  methods: {\n    discard() {\n      document.body.classList.remove('page-deleted-pending')\n      this.isShown = false\n    },\n    async deletePage() {\n      this.loading = true\n      this.$store.commit(`loadingStart`, 'page-delete')\n      this.$nextTick(async () => {\n        try {\n          const resp = await this.$apollo.mutate({\n            mutation: deletePageMutation,\n            variables: {\n              id: this.pageId\n            }\n          })\n          if (_.get(resp, 'data.pages.delete.responseResult.succeeded', false)) {\n            this.isShown = false\n            _.delay(() => {\n              document.body.classList.add('page-deleted')\n              _.delay(() => {\n                window.location.assign('/')\n              }, 1200)\n            }, 400)\n          } else {\n            throw new Error(_.get(resp, 'data.pages.delete.responseResult.message', this.$t('common:error.unexpected')))\n          }\n        } catch (err) {\n          this.$store.commit('pushGraphError', err)\n        }\n        this.$store.commit(`loadingStop`, 'page-delete')\n        this.loading = false\n      })\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n  body.page-deleted-pending {\n    perspective: 50vw;\n    height: 100vh;\n    overflow: hidden;\n\n    .application {\n      background-color: mc('grey', '900');\n    }\n    .application--wrap {\n      transform-style: preserve-3d;\n      transform: translateZ(-5vw) rotateX(2deg);\n      border-radius: 7px;\n      overflow: hidden;\n    }\n  }\n  body.page-deleted {\n    perspective: 50vw;\n\n    .application--wrap {\n      transform-style: preserve-3d;\n      transform: translateZ(-1000vw) rotateX(60deg);\n      opacity: 0;\n    }\n  }\n</style>\n"
  },
  {
    "path": "client/components/common/page-selector.vue",
    "content": "<template lang=\"pug\">\n  v-dialog(\n    v-model='isShown'\n    max-width='850px'\n    overlay-color='blue darken-4'\n    overlay-opacity='.7'\n    )\n    v-card.page-selector\n      .dialog-header.is-blue\n        v-icon.mr-3(color='white') mdi-page-next-outline\n        .body-1(v-if='mode === `create`') {{$t('common:pageSelector.createTitle')}}\n        .body-1(v-else-if='mode === `move`') {{$t('common:pageSelector.moveTitle')}}\n        .body-1(v-else-if='mode === `select`') {{$t('common:pageSelector.selectTitle')}}\n        v-spacer\n        v-progress-circular(\n          indeterminate\n          color='white'\n          :size='20'\n          :width='2'\n          v-show='searchLoading'\n          )\n      .d-flex\n        v-flex.grey(xs5, :class='$vuetify.theme.dark ? `darken-4` : `lighten-3`')\n          v-toolbar(color='grey darken-3', dark, dense, flat)\n            .body-2 {{$t('common:pageSelector.virtualFolders')}}\n            v-spacer\n            v-btn(icon, tile, href='https://docs.requarks.io/guide/pages#folders', target='_blank')\n              v-icon mdi-help-box\n          div(style='height:400px;')\n            vue-scroll(:ops='scrollStyle')\n              v-treeview(\n                :key='`pageTree-` + treeViewCacheId'\n                :active.sync='currentNode'\n                :open.sync='openNodes'\n                :items='tree'\n                :load-children='fetchFolders'\n                dense\n                expand-icon='mdi-menu-down-outline'\n                item-id='path'\n                item-text='title'\n                activatable\n                hoverable\n                )\n                template(slot='prepend', slot-scope='{ item, open, leaf }')\n                  v-icon mdi-{{ open ? 'folder-open' : 'folder' }}\n        v-flex(xs7)\n          v-toolbar(color='blue darken-2', dark, dense, flat)\n            .body-2 {{$t('common:pageSelector.pages')}}\n            //- v-spacer\n            //- v-btn(icon, tile, disabled): v-icon mdi-content-save-move-outline\n            //- v-btn(icon, tile, disabled): v-icon mdi-trash-can-outline\n          div(v-if='currentPages.length > 0', style='height:400px;')\n            vue-scroll(:ops='scrollStyle')\n              v-list.py-0(dense)\n                v-list-item-group(\n                  v-model='currentPage'\n                  color='primary'\n                  )\n                  template(v-for='(page, idx) of currentPages')\n                    v-list-item(:key='`page-` + page.id', :value='page')\n                      v-list-item-icon: v-icon mdi-text-box\n                      v-list-item-title {{page.title}}\n                    v-divider(v-if='idx < pages.length - 1')\n          v-alert.animated.fadeIn(\n            v-else\n            text\n            color='orange'\n            prominent\n            icon='mdi-alert'\n            )\n            .body-2 {{$t('common:pageSelector.folderEmptyWarning')}}\n      v-card-actions.grey.pa-2(:class='$vuetify.theme.dark ? `darken-2` : `lighten-1`', v-if='!mustExist')\n        v-select(\n          solo\n          dark\n          flat\n          background-color='grey darken-3-d2'\n          hide-details\n          single-line\n          :items='namespaces'\n          style='flex: 0 0 100px; border-radius: 4px 0 0 4px;'\n          v-model='currentLocale'\n          )\n        v-text-field(\n          ref='pathIpt'\n          solo\n          hide-details\n          prefix='/'\n          v-model='currentPath'\n          flat\n          clearable\n          style='border-radius: 0 4px 4px 0;'\n        )\n      v-card-chin\n        v-spacer\n        v-btn(text, @click='close') {{$t('common:actions.cancel')}}\n        v-btn.px-4(color='primary', @click='open', :disabled='!isValidPath')\n          v-icon(left) mdi-check\n          span {{$t('common:actions.select')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nconst localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i\n\n/* global siteLangs, siteConfig */\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    },\n    path: {\n      type: String,\n      default: 'new-page'\n    },\n    locale: {\n      type: String,\n      default: 'en'\n    },\n    mode: {\n      type: String,\n      default: 'create'\n    },\n    openHandler: {\n      type: Function,\n      default: () => {}\n    },\n    mustExist: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      treeViewCacheId: 0,\n      searchLoading: false,\n      currentLocale: siteConfig.lang,\n      currentFolderPath: '',\n      currentPath: 'new-page',\n      currentPage: null,\n      currentNode: [0],\n      openNodes: [0],\n      tree: [\n        {\n          id: 0,\n          title: '/ (root)',\n          children: []\n        }\n      ],\n      pages: [],\n      all: [],\n      namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],\n      scrollStyle: {\n        vuescroll: {},\n        scrollPanel: {\n          initialScrollX: 0.01, // fix scrollbar not disappearing on load\n          scrollingX: false,\n          speed: 50\n        },\n        rail: {\n          gutterOfEnds: '2px'\n        },\n        bar: {\n          onlyShowBarOnScroll: false,\n          background: '#999',\n          hoverStyle: {\n            background: '#64B5F6'\n          }\n        }\n      }\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    currentPages () {\n      return _.sortBy(_.filter(this.pages, ['parent', _.head(this.currentNode) || 0]), ['title', 'path'])\n    },\n    isValidPath () {\n      if (!this.currentPath) {\n        return false\n      }\n      if (this.mustExist && !this.currentPage) {\n        return false\n      }\n      const firstSection = _.head(this.currentPath.split('/'))\n      if (firstSection.length <= 1) {\n        return false\n      } else if (localeSegmentRegex.test(firstSection)) {\n        return false\n      } else if (\n        _.some(['login', 'logout', 'register', 'verify', 'favicons', 'fonts', 'img', 'js', 'svg'], p => {\n          return p === firstSection\n        })) {\n        return false\n      } else {\n        return true\n      }\n    }\n  },\n  watch: {\n    isShown (newValue, oldValue) {\n      if (newValue && !oldValue) {\n        this.currentPath = this.path\n        this.currentLocale = this.locale\n        _.delay(() => {\n          this.$refs.pathIpt.focus()\n        })\n      }\n    },\n    currentNode (newValue, oldValue) {\n      if (newValue.length < 1) { // force a selection\n        this.$nextTick(() => {\n          this.currentNode = oldValue\n        })\n      } else {\n        const current = _.find(this.all, ['id', newValue[0]])\n\n        if (this.openNodes.indexOf(newValue[0]) < 0) { // auto open and load children\n          if (current) {\n            if (this.openNodes.indexOf(current.parent) < 0) {\n              this.$nextTick(() => {\n                this.openNodes.push(current.parent)\n              })\n            }\n          }\n          this.$nextTick(() => {\n            this.openNodes.push(newValue[0])\n          })\n        }\n\n        this.currentPath = _.compact([_.get(current, 'path', ''), _.last(this.currentPath.split('/'))]).join('/')\n      }\n    },\n    currentPage (newValue, oldValue) {\n      if (!_.isEmpty(newValue)) {\n        this.currentPath = newValue.path\n      }\n    },\n    currentLocale (newValue, oldValue) {\n      this.$nextTick(() => {\n        this.tree = [\n          {\n            id: 0,\n            title: '/ (root)',\n            children: []\n          }\n        ]\n        this.currentNode = [0]\n        this.openNodes = [0]\n        this.pages = []\n        this.all = []\n        this.treeViewCacheId += 1\n      })\n    }\n  },\n  methods: {\n    close() {\n      this.isShown = false\n    },\n    open() {\n      const exit = this.openHandler({\n        locale: this.currentLocale,\n        path: this.currentPath,\n        id: (this.mustExist && this.currentPage) ? this.currentPage.pageId : 0\n      })\n      if (exit !== false) {\n        this.close()\n      }\n    },\n    async fetchFolders (item) {\n      this.searchLoading = true\n      const resp = await this.$apollo.query({\n        query: gql`\n          query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) {\n            pages {\n              tree(parent: $parent, mode: $mode, locale: $locale) {\n                id\n                path\n                title\n                isFolder\n                pageId\n                parent\n              }\n            }\n          }\n        `,\n        fetchPolicy: 'network-only',\n        variables: {\n          parent: item.id,\n          mode: 'ALL',\n          locale: this.currentLocale\n        }\n      })\n      const items = _.get(resp, 'data.pages.tree', [])\n      const itemFolders = _.filter(items, ['isFolder', true]).map(f => ({...f, children: []}))\n      const itemPages = _.filter(items, i => i.pageId > 0)\n      if (itemFolders.length > 0) {\n        item.children = itemFolders\n      } else {\n        item.children = undefined\n      }\n      this.pages = _.unionBy(this.pages, itemPages, 'id')\n      this.all = _.unionBy(this.all, items, 'id')\n\n      this.searchLoading = false\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.page-selector {\n  .v-treeview-node__label {\n    font-size: 13px;\n  }\n  .v-treeview-node__content {\n    cursor: pointer;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/common/password-strength.vue",
    "content": "<template lang=\"pug\">\n  .password-strength\n    v-progress-linear(\n      :color='passwordStrengthColor'\n      v-model='passwordStrength'\n      height='2'\n    )\n    .caption(v-if='!hideText', :class='passwordStrengthColor + \"--text\"') {{passwordStrengthText}}\n</template>\n\n<script>\nimport zxcvbn from 'zxcvbn'\nimport _ from 'lodash'\n\nexport default {\n  props: {\n    value: {\n      type: String,\n      default: ''\n    },\n    hideText: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      passwordStrength: 0,\n      passwordStrengthColor: 'grey',\n      passwordStrengthText: ''\n    }\n  },\n  watch: {\n    value(newValue) {\n      this.checkPasswordStrength(newValue)\n    }\n  },\n  methods: {\n    checkPasswordStrength: _.debounce(function (pwd) {\n      if (!pwd || pwd.length < 1) {\n        this.passwordStrength = 0\n        this.passwordStrengthColor = 'grey'\n        this.passwordStrengthText = ''\n        return\n      }\n      const strength = zxcvbn(pwd)\n      this.passwordStrength = _.round((strength.score + 1) / 5 * 100)\n      if (this.passwordStrength <= 20) {\n        this.passwordStrengthColor = 'red'\n        this.passwordStrengthText = this.$t('common:password.veryWeak')\n      } else if (this.passwordStrength <= 40) {\n        this.passwordStrengthColor = 'orange'\n        this.passwordStrengthText = this.$t('common:password.weak')\n      } else if (this.passwordStrength <= 60) {\n        this.passwordStrengthColor = 'teal'\n        this.passwordStrengthText = this.$t('common:password.average')\n      } else if (this.passwordStrength <= 80) {\n        this.passwordStrengthColor = 'green'\n        this.passwordStrengthText = this.$t('common:password.strong')\n      } else {\n        this.passwordStrengthColor = 'green'\n        this.passwordStrengthText = this.$t('common:password.veryStrong')\n      }\n    }, 100)\n  }\n}\n</script>\n\n<style lang=\"scss\">\n\n.password-strength > .caption {\n  width: 100%;\n  left: 0;\n  margin: 0;\n  position: absolute;\n  top: calc(100% + 5px);\n}\n\n</style>\n"
  },
  {
    "path": "client/components/common/search-results.vue",
    "content": "<template lang=\"pug\">\n  .search-results(v-if='searchIsFocused || (search && search.length > 1)')\n    .search-results-container\n      .search-results-help(v-if='!search || (search && search.length < 2)')\n        img(src='/_assets/svg/icon-search-alt.svg')\n        .mt-4 {{$t('common:header.searchHint')}}\n      .search-results-loader(v-else-if='searchIsLoading && (!results || results.length < 1)')\n        orbit-spinner(\n          :animation-duration='1000'\n          :size='100'\n          color='#FFF'\n        )\n        .headline.mt-5 {{$t('common:header.searchLoading')}}\n      .search-results-none(v-else-if='!searchIsLoading && (!results || results.length < 1)')\n        img(src='/_assets/svg/icon-no-results.svg', alt='No Results')\n        .subheading {{$t('common:header.searchNoResult')}}\n      template(v-if='search && search.length >= 2 && results && results.length > 0')\n        v-subheader.white--text {{$t('common:header.searchResultsCount', { total: response.totalHits })}}\n        v-list.search-results-items.radius-7.py-0(two-line, dense)\n          template(v-for='(item, idx) of results')\n            v-list-item(@click='goToPage(item)', @click.middle=\"goToPageInNewTab(item)\", :key='item.id', :class='idx === cursor ? `highlighted` : ``')\n              v-list-item-avatar(tile)\n                img(src='/_assets/svg/icon-selective-highlighting.svg')\n              v-list-item-content\n                v-list-item-title(v-text='item.title')\n                v-list-item-subtitle.caption(v-text='item.description')\n                .caption.grey--text(v-text='item.path')\n              v-list-item-action\n                v-chip(label, outlined) {{item.locale.toUpperCase()}}\n            v-divider(v-if='idx < results.length - 1')\n        v-pagination.mt-3(\n          v-if='paginationLength > 1'\n          dark\n          v-model='pagination'\n          :length='paginationLength'\n          circle\n        )\n      template(v-if='suggestions && suggestions.length > 0')\n        v-subheader.white--text.mt-3 {{$t('common:header.searchDidYouMean')}}\n        v-list.search-results-suggestions.radius-7(dense, dark)\n          template(v-for='(term, idx) of suggestions')\n            v-list-item(:key='term', @click='setSearchTerm(term)', :class='idx + results.length === cursor ? `highlighted` : ``')\n              v-list-item-avatar\n                v-icon mdi-magnify\n              v-list-item-content\n                v-list-item-title(v-text='term')\n            v-divider(v-if='idx < suggestions.length - 1')\n      .text-xs-center.pt-5(v-if='search && search.length > 1')\n        //- v-btn.mx-2(outlined, color='orange', @click='search = ``', v-if='results.length > 0')\n        //-   v-icon(left) mdi-content-save\n        //-   span {{$t('common:header.searchCopyLink')}}\n        v-btn.mx-2(outlined, color='pink', @click='search = ``')\n          v-icon(left) mdi-close\n          span {{$t('common:header.searchClose')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync } from 'vuex-pathify'\nimport { OrbitSpinner } from 'epic-spinners'\n\nimport searchPagesQuery from 'gql/common/common-pages-query-search.gql'\n\nexport default {\n  components: {\n    OrbitSpinner\n  },\n  data() {\n    return {\n      cursor: 0,\n      pagination: 1,\n      perPage: 10,\n      response: {\n        results: [],\n        suggestions: [],\n        totalHits: 0\n      }\n    }\n  },\n  computed: {\n    search: sync('site/search'),\n    searchIsFocused: sync('site/searchIsFocused'),\n    searchIsLoading: sync('site/searchIsLoading'),\n    searchRestrictLocale: sync('site/searchRestrictLocale'),\n    searchRestrictPath: sync('site/searchRestrictPath'),\n    results() {\n      const currentIndex = (this.pagination - 1) * this.perPage\n      return this.response.results ? _.slice(this.response.results, currentIndex, currentIndex + this.perPage) : []\n    },\n    hits() {\n      return this.response.totalHits ? this.response.totalHits : 0\n    },\n    suggestions() {\n      return this.response.suggestions ? this.response.suggestions : []\n    },\n    paginationLength() {\n      return (this.response.totalHits > 0) ? Math.ceil(this.response.totalHits / this.perPage) : 0\n    }\n  },\n  watch: {\n    search(newValue, oldValue) {\n      this.cursor = 0\n      if (!newValue || (newValue && newValue.length < 2)) {\n        this.searchIsLoading = false\n      } else {\n        this.searchIsLoading = true\n      }\n    },\n    results() {\n      this.cursor = 0\n    }\n  },\n  mounted() {\n    this.$root.$on('searchMove', (dir) => {\n      this.cursor += ((dir === 'up') ? -1 : 1)\n      if (this.cursor < -1) {\n        this.cursor = -1\n      } else if (this.cursor > this.results.length + this.suggestions.length - 1) {\n        this.cursor = this.results.length + this.suggestions.length - 1\n      }\n    })\n    this.$root.$on('searchEnter', () => {\n      if (!this.results) {\n        return\n      }\n\n      if (this.cursor >= 0 && this.cursor < this.results.length) {\n        this.goToPage(_.nth(this.results, this.cursor))\n      } else if (this.cursor >= 0) {\n        this.setSearchTerm(_.nth(this.suggestions, this.cursor - this.results.length))\n      }\n    })\n  },\n  methods: {\n    setSearchTerm(term) {\n      this.search = term\n    },\n    goToPage(item) {\n      window.location.assign(`/${item.locale}/${item.path}`)\n    },\n    goToPageInNewTab(item) {\n      window.open(`/${item.locale}/${item.path}`, '_blank')\n    }\n  },\n  apollo: {\n    response: {\n      query: searchPagesQuery,\n      variables() {\n        return {\n          query: this.search\n        }\n      },\n      fetchPolicy: 'network-only',\n      debounce: 300,\n      throttle: 1000,\n      skip() {\n        return !this.search || this.search.length < 2\n      },\n      result() {\n        this.pagination = 1\n      },\n      update: (data) => _.get(data, 'pages.search', {}),\n      watchLoading (isLoading) {\n        this.searchIsLoading = isLoading\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.search-results {\n  position: fixed;\n  top: 64px;\n  left: 0;\n  overflow-y: auto;\n  width: 100%;\n  height: calc(100% - 64px);\n  background-color: rgba(0,0,0,.9);\n  z-index: 100;\n  text-align: center;\n  animation: searchResultsReveal .6s ease;\n\n  @media #{map-get($display-breakpoints, 'sm-and-down')} {\n    top: 112px;\n  }\n\n  &-container {\n    margin: 12px auto;\n    width: 90vw;\n    max-width: 1024px;\n  }\n\n  &-help {\n    text-align: center;\n    padding: 32px 0;\n    font-size: 18px;\n    font-weight: 300;\n    color: #FFF;\n\n    img {\n      width: 104px;\n    }\n  }\n\n  &-loader {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    flex-direction: column;\n    padding: 32px 0;\n    color: #FFF;\n  }\n\n  &-none {\n    color: #FFF;\n\n    img {\n      width: 200px;\n    }\n  }\n\n  &-items {\n    text-align: left;\n\n    .highlighted {\n      background: #FFF linear-gradient(to bottom, #FFF, mc('orange', '100'));\n\n      @at-root .theme--dark & {\n        background: mc('grey', '900') linear-gradient(to bottom, mc('orange', '900'), darken(mc('orange', '900'), 15%));\n      }\n    }\n  }\n\n  &-suggestions {\n    .highlighted {\n      background: transparent linear-gradient(to bottom, mc('blue', '500'), mc('blue', '700'));\n    }\n  }\n}\n\n@keyframes searchResultsReveal {\n  0% {\n    background-color: rgba(0,0,0,0);\n    padding-top: 32px;\n  }\n  100% {\n    background-color: rgba(0,0,0,.9);\n    padding-top: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/common/social-sharing.vue",
    "content": "<template lang=\"pug\">\n  v-list(nav, dense)\n    v-list-item(@click='', ref='copyUrlButton')\n      v-icon(color='grey', small) mdi-content-copy\n      v-list-item-title.px-3 Copy URL\n    v-list-item(:href='`mailto:?subject=` + encodeURIComponent(title) + `&body=` + encodeURIComponent(url) + `%0D%0A%0D%0A` + encodeURIComponent(description)')\n      v-icon(color='grey', small) mdi-email-outline\n      v-list-item-title.px-3 Email\n    v-list-item(@click='openSocialPop(`https://www.facebook.com/sharer/sharer.php?u=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title) + `&description=` + encodeURIComponent(description))')\n      v-icon(color='grey', small) mdi-facebook\n      v-list-item-title.px-3 Facebook\n    v-list-item(@click='openSocialPop(`https://www.linkedin.com/shareArticle?mini=true&url=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title) + `&summary=` + encodeURIComponent(description))')\n      v-icon(color='grey', small) mdi-linkedin\n      v-list-item-title.px-3 LinkedIn\n    v-list-item(@click='openSocialPop(`https://www.reddit.com/submit?url=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title))')\n      v-icon(color='grey', small) mdi-reddit\n      v-list-item-title.px-3 Reddit\n    v-list-item(@click='openSocialPop(`https://t.me/share/url?url=` + encodeURIComponent(url) + `&text=` + encodeURIComponent(title))')\n      v-icon(color='grey', small) mdi-telegram\n      v-list-item-title.px-3 Telegram\n    v-list-item(@click='openSocialPop(`https://twitter.com/intent/tweet?url=` + encodeURIComponent(url) + `&text=` + encodeURIComponent(title))')\n      v-icon(color='grey', small) mdi-twitter\n      v-list-item-title.px-3 Twitter\n    v-list-item(:href='`viber://forward?text=` + encodeURIComponent(url) + ` ` + encodeURIComponent(description)')\n      v-icon(color='grey', small) mdi-phone-in-talk\n      v-list-item-title.px-3 Viber\n    v-list-item(@click='openSocialPop(`http://service.weibo.com/share/share.php?url=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title))')\n      v-icon(color='grey', small) mdi-sina-weibo\n      v-list-item-title.px-3 Weibo\n    v-list-item(@click='openSocialPop(`https://api.whatsapp.com/send?text=` + encodeURIComponent(title) + `%0D%0A` + encodeURIComponent(url))')\n      v-icon(color='grey', small) mdi-whatsapp\n      v-list-item-title.px-3 Whatsapp\n</template>\n\n<script>\nimport ClipboardJS from 'clipboard'\n\nexport default {\n  props: {\n    url: {\n      type: String,\n      default: window.location.url\n    },\n    title: {\n      type: String,\n      default: 'Untitled Page'\n    },\n    description: {\n      type: String,\n      default: ''\n    }\n  },\n  data () {\n    return {\n      width: 626,\n      height: 436,\n      left: 0,\n      top: 0\n    }\n  },\n  methods: {\n    openSocialPop (url) {\n      const popupWindow = window.open(\n        url,\n        'sharer',\n        `status=no,height=${this.height},width=${this.width},resizable=yes,left=${this.left},top=${this.top},screenX=${this.left},screenY=${this.top},toolbar=no,menubar=no,scrollbars=no,location=no,directories=no`\n      )\n\n      popupWindow.focus()\n    }\n  },\n  mounted () {\n    const clip = new ClipboardJS(this.$refs.copyUrlButton.$el, {\n      text: () => { return this.url }\n    })\n\n    clip.on('success', () => {\n      this.$store.commit('showNotification', {\n        style: 'success',\n        message: `URL copied successfully`,\n        icon: 'content-copy'\n      })\n    })\n    clip.on('error', () => {\n      this.$store.commit('showNotification', {\n        style: 'red',\n        message: `Failed to copy to clipboard`,\n        icon: 'alert'\n      })\n    })\n\n    /**\n     * Center the popup on dual screens\n     * http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen/32261263\n     */\n    const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left\n    const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top\n\n    const width = window.innerWidth ? window.innerWidth : (document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width)\n    const height = window.innerHeight ? window.innerHeight : (document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height)\n\n    this.left = ((width / 2) - (this.width / 2)) + dualScreenLeft\n    this.top = ((height / 2) - (this.height / 2)) + dualScreenTop\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/common/user-search.vue",
    "content": "<template lang=\"pug\">\n  v-dialog(\n    v-model='dialogOpen'\n    max-width='650'\n    )\n    v-card\n      .dialog-header\n        span {{$t('common:user.search')}}\n        v-spacer\n        v-progress-circular(\n          indeterminate\n          color='white'\n          :size='20'\n          :width='2'\n          v-show='searchLoading'\n          )\n      v-card-text.pt-5\n        v-text-field(\n          outlined\n          :label='$t(`common:user.searchPlaceholder`)'\n          v-model='search'\n          prepend-inner-icon='mdi-account-search-outline'\n          color='primary'\n          ref='searchIpt'\n          hide-details\n          )\n        v-list.grey.mt-3.py-0.radius-7(\n          :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`'\n          two-line\n          dense\n          )\n          template(v-for='(usr, idx) in items')\n            v-list-item(:key='usr.id', @click='setUser(usr)')\n              v-list-item-avatar(size='40', color='primary')\n                span.body-1.white--text {{usr.name | initials}}\n              v-list-item-content\n                v-list-item-title.body-2 {{usr.name}}\n                v-list-item-subtitle {{usr.email}}\n              v-list-item-action\n                v-icon(color='primary') mdi-arrow-right\n            v-divider.my-0(v-if='idx < items.length - 1')\n      v-card-chin\n        v-spacer\n        v-btn(\n          text\n          @click='close'\n          :disabled='loading'\n          ) {{$t('common:actions.cancel')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nexport default {\n  filters: {\n    initials(val) {\n      return val.split(' ').map(v => v.substring(0, 1)).join('')\n    }\n  },\n  props: {\n    multiple: {\n      type: Boolean,\n      default: false\n    },\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      loading: false,\n      searchLoading: false,\n      search: '',\n      items: []\n    }\n  },\n  computed: {\n    dialogOpen: {\n      get() { return this.value },\n      set(value) { this.$emit('input', value) }\n    }\n  },\n  watch: {\n    value(newValue, oldValue) {\n      if (newValue && !oldValue) {\n        this.search = ''\n        this.selectedItems = null\n        _.delay(() => { this.$refs.searchIpt.focus() }, 100)\n      }\n    }\n  },\n  methods: {\n    close() {\n      this.$emit('input', false)\n    },\n    setUser(usr) {\n      this.$emit('select', usr)\n      this.close()\n    },\n    searchFilter(item, queryText, itemText) {\n      return _.includes(_.toLower(item.email), _.toLower(queryText)) || _.includes(_.toLower(item.name), _.toLower(queryText))\n    }\n  },\n  apollo: {\n    items: {\n      query: gql`\n        query ($query: String!) {\n          users {\n            search(query:$query) {\n              id\n              name\n              email\n              providerKey\n            }\n          }\n        }\n      `,\n      variables() {\n        return {\n          query: this.search\n        }\n      },\n      fetchPolicy: 'cache-and-network',\n      skip() {\n        return !this.search || this.search.length < 2\n      },\n      update: (data) => data.users.search,\n      watchLoading (isLoading) {\n        this.searchLoading = isLoading\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/common/v-card-chin.vue",
    "content": "<template lang='pug'>\n  div\n    v-divider.my-0\n    v-card-actions(:class='$vuetify.theme.dark ? \"grey darken-4-l5\" : \"grey lighten-4\"')\n      slot\n</template>\n\n<script>\nexport default { }\n</script>\n"
  },
  {
    "path": "client/components/common/v-card-info.vue",
    "content": "<template lang='pug'>\n  .v-card-info(:class='`is-` + color')\n    v-card-text.d-flex.align-center(:class='colors.cls')\n      v-icon(:color='colors.icon', left) {{icon}}\n      slot\n</template>\n\n<script>\nexport default {\n  props: {\n    color: {\n      type: String,\n      default: 'blue'\n    },\n    icon: {\n      type: String,\n      default: 'mdi-information-outline'\n    }\n  },\n  computed: {\n    colors () {\n      switch (this.color) {\n        case 'blue':\n          return {\n            cls: this.$vuetify.theme.dark ? 'grey darken-4-l5 blue--text text--lighten-4' : 'blue lighten-5 blue--text text--darken-3',\n            icon: 'blue lighten-3'\n          }\n        case 'red':\n          return {\n            cls: this.$vuetify.theme.dark ? 'grey darken-4-l5 red--text text--lighten-4' : 'red lighten-5 red--text text--darken-2',\n            icon: 'red lighten-3'\n          }\n        default:\n          return {\n            cls: this.$vuetify.theme.dark ? 'grey darken-4-l5' : 'grey lighten-4',\n            icon: 'grey darken-2'\n          }\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.v-card-info {\n  border-bottom: 1px solid #EEE;\n\n  &.is-blue {\n    border-bottom-color: mc('blue', '100');\n\n    @at-root .theme--dark & {\n      border-bottom-color: rgba(mc('blue', '100'), .3);\n    }\n  }\n\n  &.is-red {\n    border-bottom-color: mc('red', '100');\n\n    @at-root .theme--dark & {\n      border-bottom-color: rgba(mc('red', '100'), .3);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/api/server-selector.vue",
    "content": "<template lang=\"pug\">\n\n</template>\n\n<script>\nexport default {\n\n}\n</script>\n"
  },
  {
    "path": "client/components/editor/ckeditor/conflict.vue",
    "content": "<template lang=\"pug\">\n  v-dialog(\n    v-model='isShown'\n    max-width='700'\n    )\n    v-card\n      .dialog-header.is-short.is-indigo\n        v-icon.mr-2(color='white') mdi-alert\n        span {{$t('editor:conflict.title')}}\n      v-card-text.pt-4\n        i18next.body-2(tag='div', path='editor:conflict.infoGeneric')\n          strong(place='authorName') {{latest.authorName}}\n          span(place='date', :title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}.\n        v-btn.mt-2(outlined, color='indigo', small, :href='`/` + latest.locale + `/` + latest.path', target='_blank')\n          v-icon(left) mdi-open-in-new\n          span {{$t('editor:conflict.viewLatestVersion')}}\n        .body-2.mt-5: strong {{$t('editor:conflict.whatToDo')}}\n        .body-2.mt-1 #[v-icon(color='indigo') mdi-alpha-l-box] {{$t('editor:conflict.whatToDoLocal')}}\n        .body-2.mt-1 #[v-icon(color='indigo') mdi-alpha-r-box] {{$t('editor:conflict.whatToDoRemote')}}\n      v-card-chin\n        v-spacer\n        v-btn(text, @click='close') {{$t('common:actions.cancel')}}\n        v-btn.px-4(color='indigo', @click='useLocal', dark, :title='$t(`editor:conflict.useLocalHint`)')\n          v-icon(left) mdi-alpha-l-box\n          span {{$t('editor:conflict.useLocal')}}\n        v-dialog(\n          v-model='isRemoteConfirmDiagShown'\n          width='500'\n          )\n          template(v-slot:activator='{ on }')\n            v-btn.ml-3(color='indigo', dark, v-on='on', :title='$t(`editor:conflict.useRemoteHint`)')\n              v-icon(left) mdi-alpha-r-box\n              span {{$t('editor:conflict.useRemote')}}\n          v-card\n            .dialog-header.is-short.is-indigo\n              v-icon.mr-3(color='white') mdi-alpha-r-box\n              span {{$t('editor:conflict.overwrite.title')}}\n            v-card-text.pa-4\n              i18next.body-2(tag='div', path='editor:conflict.overwrite.description')\n                strong(place='refEditsLost') {{$t('editor:conflict.overwrite.editsLost')}}\n            v-card-chin\n              v-spacer\n              v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')\n                v-icon(left) mdi-close\n                span {{$t('common:actions.cancel')}}\n              v-btn(@click='useRemote', color='indigo', dark)\n                v-icon(left) mdi-check\n                span {{$t('common:actions.confirm')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      latest: {\n        updatedAt: '',\n        authorName: '',\n        content: '',\n        locale: '',\n        path: ''\n      },\n      isRemoteConfirmDiagShown: false\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    }\n  },\n  methods: {\n    close () {\n      this.isShown = false\n    },\n    useLocal () {\n      this.$store.set('editor/checkoutDateActive', this.latest.updatedAt)\n      this.$root.$emit('resetEditorConflict')\n      this.close()\n    },\n    useRemote () {\n      this.$store.set('editor/checkoutDateActive', this.latest.updatedAt)\n      this.$store.set('editor/content', this.latest.content)\n      this.$root.$emit('overwriteEditorContent')\n      this.$root.$emit('resetEditorConflict')\n      this.close()\n    }\n  },\n  async mounted () {\n    let resp = await this.$apollo.query({\n      query: gql`\n        query ($id: Int!) {\n          pages {\n            conflictLatest(id: $id) {\n              authorName\n              locale\n              path\n              content\n              updatedAt\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      variables: {\n        id: this.$store.get('page/id')\n      }\n    })\n    resp = _.get(resp, 'data.pages.conflictLatest', false)\n\n    if (!resp) {\n      return this.$store.commit('showNotification', {\n        message: 'Failed to fetch latest version.',\n        style: 'warning',\n        icon: 'warning'\n      })\n    }\n    this.latest = resp\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/editor/common/cmFold.js",
    "content": "// Header matching code by CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/LICENSE\n\nimport CodeMirror from 'codemirror'\n\nconst maxDepth = 100\nconst codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/\nconst codeBlockEndMatch = /^`{3}$/\n\nexport default {\n  register(lang) {\n    CodeMirror.registerHelper('fold', lang, foldHandler)\n  }\n}\n\nfunction foldHandler (cm, start) {\n  const firstLine = cm.getLine(start.line)\n  const lastLineNo = cm.lastLine()\n  let end\n\n  function isHeader(lineNo) {\n    const tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0))\n    return tokentype && /\\bheader\\b/.test(tokentype)\n  }\n\n  function headerLevel(lineNo, line, nextLine) {\n    let match = line && line.match(/^#+/)\n    if (match && isHeader(lineNo)) return match[0].length\n    match = nextLine && nextLine.match(/^[=-]+\\s*$/)\n    if (match && isHeader(lineNo + 1)) return nextLine[0] === '=' ? 1 : 2\n    return maxDepth\n  }\n\n  // -> CODE BLOCK\n\n  if (codeBlockStartMatch.test(cm.getLine(start.line))) {\n    end = start.line\n    let nextNextLine = cm.getLine(end + 1)\n    while (end < lastLineNo) {\n      if (codeBlockEndMatch.test(nextNextLine)) {\n        end++\n        break\n      }\n      end++\n      nextNextLine = cm.getLine(end + 1)\n    }\n  } else {\n    // -> HEADER\n\n    let nextLine = cm.getLine(start.line + 1)\n    const level = headerLevel(start.line, firstLine, nextLine)\n    if (level === maxDepth) return undefined\n\n    end = start.line\n    let nextNextLine = cm.getLine(end + 2)\n    while (end < lastLineNo) {\n      if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break\n      ++end\n      nextLine = nextNextLine\n      nextNextLine = cm.getLine(end + 2)\n    }\n  }\n\n  return {\n    from: CodeMirror.Pos(start.line, firstLine.length),\n    to: CodeMirror.Pos(end, cm.getLine(end).length)\n  }\n}\n"
  },
  {
    "path": "client/components/editor/common/katex.js",
    "content": "// Test if potential opening or closing delimieter\n// Assumes that there is a \"$\" at state.src[pos]\nfunction isValidDelim (state, pos) {\n  let prevChar\n  let nextChar\n  let max = state.posMax\n  let canOpen = true\n  let canClose = true\n\n  prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1\n  nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1\n\n  // Check non-whitespace conditions for opening and closing, and\n  // check that closing delimeter isn't followed by a number\n  if (prevChar === 0x20/* \" \" */ || prevChar === 0x09/* \\t */ ||\n          (nextChar >= 0x30/* \"0\" */ && nextChar <= 0x39/* \"9\" */)) {\n    canClose = false\n  }\n  if (nextChar === 0x20/* \" \" */ || nextChar === 0x09/* \\t */) {\n    canOpen = false\n  }\n\n  return {\n    canOpen: canOpen,\n    canClose: canClose\n  }\n}\n\nexport default {\n  katexInline (state, silent) {\n    let start, match, token, res, pos\n\n    if (state.src[state.pos] !== '$') { return false }\n\n    res = isValidDelim(state, state.pos)\n    if (!res.canOpen) {\n      if (!silent) { state.pending += '$' }\n      state.pos += 1\n      return true\n    }\n\n    // First check for and bypass all properly escaped delimieters\n    // This loop will assume that the first leading backtick can not\n    // be the first character in state.src, which is known since\n    // we have found an opening delimieter already.\n    start = state.pos + 1\n    match = start\n    while ((match = state.src.indexOf('$', match)) !== -1) {\n      // Found potential $, look for escapes, pos will point to\n      // first non escape when complete\n      pos = match - 1\n      while (state.src[pos] === '\\\\') { pos -= 1 }\n\n      // Even number of escapes, potential closing delimiter found\n      if (((match - pos) % 2) === 1) { break }\n      match += 1\n    }\n\n    // No closing delimter found.  Consume $ and continue.\n    if (match === -1) {\n      if (!silent) { state.pending += '$' }\n      state.pos = start\n      return true\n    }\n\n    // Check if we have empty content, ie: $$.  Do not parse.\n    if (match - start === 0) {\n      if (!silent) { state.pending += '$$' }\n      state.pos = start + 1\n      return true\n    }\n\n    // Check for valid closing delimiter\n    res = isValidDelim(state, match)\n    if (!res.canClose) {\n      if (!silent) { state.pending += '$' }\n      state.pos = start\n      return true\n    }\n\n    if (!silent) {\n      token = state.push('katex_inline', 'math', 0)\n      token.markup = '$'\n      token.content = state.src\n        // Extract the math part without the $\n        .slice(start, match)\n        // Escape the curly braces since they will be interpreted as\n        // attributes by markdown-it-attrs (the \"curly_attributes\"\n        // core rule)\n        .replaceAll(\"{\", \"{{\")\n        .replaceAll(\"}\", \"}}\")\n    }\n\n    state.pos = match + 1\n    return true\n  },\n\n  katexBlock (state, start, end, silent) {\n    let firstLine; let lastLine; let next; let lastPos; let found = false; let token\n    let pos = state.bMarks[start] + state.tShift[start]\n    let max = state.eMarks[start]\n\n    if (pos + 2 > max) { return false }\n    if (state.src.slice(pos, pos + 2) !== '$$') { return false }\n\n    pos += 2\n    firstLine = state.src.slice(pos, max)\n\n    if (silent) { return true }\n    if (firstLine.trim().slice(-2) === '$$') {\n      // Single line expression\n      firstLine = firstLine.trim().slice(0, -2)\n      found = true\n    }\n\n    for (next = start; !found;) {\n      next++\n\n      if (next >= end) { break }\n\n      pos = state.bMarks[next] + state.tShift[next]\n      max = state.eMarks[next]\n\n      if (pos < max && state.tShift[next] < state.blkIndent) {\n        // non-empty line with negative indent should stop the list:\n        break\n      }\n\n      if (state.src.slice(pos, max).trim().slice(-2) === '$$') {\n        lastPos = state.src.slice(0, max).lastIndexOf('$$')\n        lastLine = state.src.slice(pos, lastPos)\n        found = true\n      }\n    }\n\n    state.line = next + 1\n\n    token = state.push('katex_block', 'math', 0)\n    token.block = true\n    token.content = (firstLine && firstLine.trim() ? firstLine + '\\n' : '') +\n    state.getLines(start + 1, next, state.tShift[start], true) +\n    (lastLine && lastLine.trim() ? lastLine : '')\n    token.map = [ start, state.line ]\n    token.markup = '$$'\n    return true\n  }\n}\n"
  },
  {
    "path": "client/components/editor/editor-api.vue",
    "content": "<template lang='pug'>\n  .editor-api\n    .editor-api-main\n      v-list.editor-api-sidebar.radius-0(nav, :class='$vuetify.theme.dark ? `grey darken-4` : `primary`', dark)\n        v-list-item-group(v-model='tab')\n          v-list-item.animated.fadeInLeft(value='info')\n            v-list-item-icon: v-icon mdi-book-information-variant\n            v-list-item-title Info\n          v-list-item.mt-3.animated.fadeInLeft.wait-p2s(value='servers')\n            v-list-item-icon: v-icon mdi-server\n            v-list-item-title Servers\n          v-list-item.mt-3.animated.fadeInLeft.wait-p3s(value='endpoints')\n            v-list-item-icon: v-icon mdi-code-braces\n            v-list-item-title Endpoints\n          v-list-item.mt-3.animated.fadeInLeft.wait-p4s(value='models')\n            v-list-item-icon: v-icon mdi-buffer\n            v-list-item-title Models\n          v-list-item.mt-3.animated.fadeInLeft.wait-p5s(value='auth')\n            v-list-item-icon: v-icon mdi-lock\n            v-list-item-title Authentication\n      .editor-api-editor\n        template(v-if='tab === `info`')\n          v-container.px-2.pt-1(fluid)\n            v-row(dense)\n              v-col(cols='12')\n                .pa-3\n                  .subtitle-2 API General Information\n                  .caption.grey--text.text--darken-1 Global metadata about the API\n              v-col(cols='12', lg='6')\n                v-card.pt-2\n                  v-card-text\n                    v-text-field(\n                      label='Title'\n                      outlined\n                      hint='Required - Title of the API'\n                      persistent-hint\n                      v-model='info.title'\n                    )\n                    v-divider.mt-2.mb-4\n                    v-text-field(\n                      label='Version'\n                      outlined\n                      hint='Required - Semantic versioning like 1.0.0 or an arbitrary string like 0.99-beta.'\n                      persistent-hint\n                      v-model='info.version'\n                    )\n                    v-divider.mt-2.mb-4\n                    v-textarea(\n                      label='Description'\n                      outlined\n                      hint='Optional - Markdown formatting is supported.'\n                      persistent-hint\n                      v-model='info.description'\n                    )\n              v-col(cols='12', lg='6')\n                v-card.pt-2\n                  v-card-text\n                    v-list(nav, two-line)\n                      v-list-item-group(v-model='kind', mandatory, color='primary')\n                        v-list-item(value='rest')\n                          v-list-item-avatar\n                            img(src='/_assets/svg/icon-transaction-list.svg', alt='REST')\n                          v-list-item-content\n                            v-list-item-title REST API\n                            v-list-item-subtitle Classic REST Endpoints\n                          v-list-item-avatar\n                            v-icon(:color='kind === `rest` ? `primary` : `grey lighten-3`') mdi-check-circle\n                        v-list-item(value='graphql', disabled)\n                          v-list-item-avatar\n                            img(src='/_assets/svg/icon-graphql.svg', alt='GraphQL')\n                          v-list-item-content\n                            v-list-item-title GraphQL\n                            v-list-item-subtitle.grey--text.text--lighten-1 Schema-based API\n                          v-list-item-action\n                            //- v-icon(:color='kind === `graphql` ? `primary` : `grey lighten-3`') mdi-check-circle\n                            v-chip(label, small) Coming soon\n        template(v-else-if='tab === `servers`')\n          v-container.px-2.pt-1(fluid)\n            v-row(dense)\n              v-col(cols='12')\n                .pa-3\n                  .d-flex.align-center.justify-space-between\n                    div\n                      .subtitle-2 List of servers / load balancers where this API reside\n                      .caption.grey--text.text--darken-1 Enter all environments, e.g. Integration, QA, Pre-production, Production, etc.\n                    v-btn(color='primary', large, @click='addServer')\n                      v-icon(left) mdi-plus\n                      span Add Server\n              v-col(cols='12', lg='6', v-for='srv of servers', :key='srv.id')\n                v-card.pt-1\n                  v-card-text\n                    .d-flex\n                      .d-flex.flex-column.justify-space-between\n                        v-menu(offset-y, min-width='200')\n                          template(v-slot:activator='{ on }')\n                            v-btn(text, x-large, style='min-width: 0;', v-on='on')\n                              v-icon(large, :color='iconColor(srv.icon)') {{iconKey(srv.icon)}}\n                          v-list(nav, dense)\n                            v-list-item-group(v-model='srv.icon', mandatory)\n                              v-list-item(:value='srvKey', v-for='(srv, srvKey) in serverTypes', :key='srvKey')\n                                v-list-item-icon: v-icon(large, :color='srv.color', v-text='srv.icon')\n                                v-list-item-content: v-list-item-title(v-text='srv.title')\n                        v-btn.mb-2(depressed, small, @click='removeServer(srv.id)')\n                          v-icon(left) mdi-close\n                          span Delete\n                      v-divider.ml-5(vertical)\n                      .pl-5(style='flex: 1 1 100%;')\n                        v-text-field(\n                          label='Environment / Server Name'\n                          outlined\n                          hint='Required - Name of the environment (e.g. QA, Production)'\n                          persistent-hint\n                          v-model='srv.name'\n                        )\n                        v-text-field.mt-4(\n                          label='URL'\n                          outlined\n                          hint='Required - URL of the environment (e.g. https://api.example.com/v1)'\n                          persistent-hint\n                          v-model='srv.url'\n                        )\n\n        template(v-else-if='tab === `endpoints`')\n          v-container.px-2.pt-1(fluid)\n            v-row(dense)\n              v-col(cols='12')\n                .pa-3\n                  .d-flex.align-center.justify-space-between\n                    div\n                      .subtitle-2 List of endpoints\n                      .caption.grey--text.text--darken-1 Groups of REST endpoints (GET, POST, PUT, DELETE).\n                    v-btn(color='primary', large, @click='addGroup')\n                      v-icon(left) mdi-plus\n                      span Add Group\n              v-col(cols='12', v-for='grp of endpointGroups', :key='grp.id')\n                v-card(color='grey darken-2')\n                  v-card-text\n                    v-toolbar(color='grey darken-2', flat, height='86')\n                      v-text-field.mr-1(\n                        flat\n                        dark\n                        label='Group Name'\n                        solo\n                        hint='Group Name'\n                        persistent-hint\n                        v-model='grp.name'\n                      )\n                      v-text-field.mx-1(\n                        flat\n                        dark\n                        label='Group Description'\n                        solo\n                        hint='Group Description'\n                        persistent-hint\n                        v-model='grp.description'\n                      )\n                      v-divider.mx-3(vertical, dark)\n                      v-btn.mx-1.align-self-start(color='grey lighten-2', @click='addEndpoint(grp)', dark, text, height='48')\n                        v-icon(left) mdi-trash-can\n                        span Delete\n                      v-divider.mx-3(vertical, dark)\n                      v-btn.ml-1.align-self-start(color='pink', @click='addEndpoint(grp)', dark, depressed, height='48')\n                        v-icon(left) mdi-plus\n                        span Add Endpoint\n                    v-container.pa-0.mt-2(fluid)\n                      v-row(dense)\n                        v-col(cols='12', v-for='ept of grp.endpoints', :key='ept.id')\n                          v-card.pt-1\n                            v-card-text\n                              .d-flex\n                                .d-flex.flex-column\n                                  v-menu(offset-y, min-width='140')\n                                    template(v-slot:activator='{ on }')\n                                      v-btn.subtitle-1(depressed, large, dark, style='min-width: 140px;', height='48', v-on='on', :color='methodColor(ept.method)')\n                                        strong {{ept.method}}\n                                    v-list(nav, dense)\n                                      v-list-item-group(v-model='ept.method', mandatory)\n                                        v-list-item(:value='mtd.key', v-for='mtd of endpointMethods', :key='mtd.key')\n                                          v-list-item-content\n                                            v-chip.text-center(label, :color='mtd.color', dark) {{mtd.key}}\n                                  v-btn.mt-2(v-if='!ept.expanded', small, @click='ept.expanded = true', color='pink', outlined)\n                                    v-icon(left) mdi-arrow-down-box\n                                    span Expand\n                                  v-btn.mt-2(v-else, small, @click='ept.expanded = false', color='pink', outlined)\n                                    v-icon(left) mdi-arrow-up-box\n                                    span Collapse\n                                  template(v-if='ept.expanded')\n                                    v-spacer\n                                    v-btn.my-2(depressed, small, @click='removeEndpoint(grp, ept.id)')\n                                      v-icon(left) mdi-close\n                                      span Delete\n                                v-divider.ml-5(vertical)\n                                .pl-5(style='flex: 1 1 100%;')\n                                  .d-flex\n                                    v-text-field.mr-2(\n                                      label='Path'\n                                      outlined\n                                      hint='Required - Path to the endpoint (e.g. /planets/{planetId})'\n                                      persistent-hint\n                                      v-model='ept.path'\n                                    )\n                                    v-text-field.ml-2(\n                                      label='Summary'\n                                      outlined\n                                      hint='Required - A short summary of the endpoint (a few words).'\n                                      persistent-hint\n                                      v-model='ept.summary'\n                                    )\n                                  template(v-if='ept.expanded')\n                                    v-text-field.mt-3(\n                                      label='Description'\n                                      outlined\n                                      v-model='ept.description'\n                                    )\n\n    v-system-bar.editor-api-sysbar(dark, status, color='grey darken-3')\n      .caption.editor-api-sysbar-locale {{locale.toUpperCase()}}\n      .caption.px-3 /{{path}}\n      template(v-if='$vuetify.breakpoint.mdAndUp')\n        v-spacer\n        .caption API Docs\n        v-spacer\n        .caption OpenAPI 3.0\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { v4 as uuid } from 'uuid'\nimport { get, sync } from 'vuex-pathify'\n\nexport default {\n  data() {\n    return {\n      tab: `endpoints`,\n      kind: 'rest',\n      kinds: [\n        { text: 'REST', value: 'rest' },\n        { text: 'GraphQL', value: 'graphql' }\n      ],\n      info: {\n        title: '',\n        version: '1.0.0',\n        description: ''\n      },\n      servers: [\n        { name: 'Production', url: 'https://api.example.com/v1', icon: 'server', id: '123456' }\n      ],\n      serverTypes: {\n        aws: {\n          color: 'orange',\n          icon: 'mdi-aws',\n          title: 'AWS'\n        },\n        azure: {\n          color: 'blue darken-2',\n          icon: 'mdi-azure',\n          title: 'Azure'\n        },\n        digitalocean: {\n          color: 'blue',\n          icon: 'mdi-digital-ocean',\n          title: 'DigitalOcean'\n        },\n        docker: {\n          color: 'blue',\n          icon: 'mdi-docker',\n          title: 'Docker'\n        },\n        google: {\n          color: 'red',\n          icon: 'mdi-google',\n          title: 'Google'\n        },\n        kubernetes: {\n          color: 'blue darken-2',\n          icon: 'mdi-kubernetes',\n          title: 'Kubernetes'\n        },\n        linux: {\n          color: 'grey darken-3',\n          icon: 'mdi-linux',\n          title: 'Linux'\n        },\n        mac: {\n          color: 'grey darken-2',\n          icon: 'mdi-apple',\n          title: 'Mac'\n        },\n        server: {\n          color: 'grey',\n          icon: 'mdi-server',\n          title: 'Server'\n        },\n        windows: {\n          color: 'blue darken-2',\n          icon: 'mdi-windows',\n          title: 'Windows'\n        }\n      },\n      endpointGroups: [\n        {\n          id: '345678',\n          name: '',\n          description: '',\n          endpoints: [\n            { method: 'GET', path: '/pet', expanded: false, id: '234567' }\n          ]\n        }\n      ],\n      endpointMethods: [\n        { key: 'GET', color: 'blue' },\n        { key: 'POST', color: 'green' },\n        { key: 'PUT', color: 'orange' },\n        { key: 'PATCH', color: 'cyan' },\n        { key: 'DELETE', color: 'red' },\n        { key: 'HEAD', color: 'deep-purple' },\n        { key: 'OPTIONS', color: 'blue-grey' }\n      ]\n    }\n  },\n  computed: {\n    isMobile() {\n      return this.$vuetify.breakpoint.smAndDown\n    },\n    locale: get('page/locale'),\n    path: get('page/path'),\n    mode: get('editor/mode'),\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n    iconColor (val) {\n      return _.get(this.serverTypes, `${val}.color`, 'white')\n    },\n    iconKey (val) {\n      return _.get(this.serverTypes, `${val}.icon`, 'mdi-server')\n    },\n    methodColor (val) {\n      return _.get(_.find(this.endpointMethods, ['key', val]), 'color', 'grey')\n    },\n    addServer () {\n      this.servers.push({\n        id: uuid(),\n        name: 'Production',\n        url: 'https://api.example.com/v1',\n        icon: 'server'\n      })\n    },\n    removeServer (id) {\n      this.servers = _.reject(this.servers, ['id', id])\n    },\n    addGroup () {\n      this.endpointGroups.push({\n        id: uuid(),\n        name: '',\n        description: '',\n        endpoints: []\n      })\n    },\n    addEndpoint (grp) {\n      grp.endpoints.push({\n        id: uuid(),\n        method: 'GET',\n        path: '/pet',\n        expanded: false\n      })\n    },\n    removeEndpoint (grp, eptId) {\n      grp.endpoints = _.reject(grp.endpoints, ['id', eptId])\n    },\n    toggleModal(key) {\n      this.activeModal = (this.activeModal === key) ? '' : key\n      this.helpShown = false\n    },\n    closeAllModal() {\n      this.activeModal = ''\n      this.helpShown = false\n    }\n  },\n  mounted() {\n    this.$store.set('editor/editorKey', 'api')\n\n    if (this.mode === 'create') {\n      this.$store.set('editor/content', '<h1>Title</h1>\\n\\n<p>Some text here</p>')\n    }\n  },\n  beforeDestroy() {\n    this.$root.$off('editorInsert')\n  }\n}\n</script>\n\n<style lang='scss'>\n$editor-height: calc(100vh - 64px - 24px);\n$editor-height-mobile: calc(100vh - 56px - 16px);\n\n.editor-api {\n  &-main {\n    display: flex;\n    width: 100%;\n  }\n\n  &-editor {\n    background-color: darken(mc('grey', '100'), 4.5%);\n    flex: 1 1 50%;\n    display: block;\n    height: $editor-height;\n    position: relative;\n\n    @at-root .theme--dark & {\n      background-color: darken(mc('grey', '900'), 4.5%);\n    }\n  }\n\n  &-sidebar {\n    width: 200px;\n  }\n\n  &-sysbar {\n    padding-left: 0 !important;\n\n    &-locale {\n      background-color: rgba(255,255,255,.25);\n      display:inline-flex;\n      padding: 0 12px;\n      height: 24px;\n      width: 63px;\n      justify-content: center;\n      align-items: center;\n    }\n  }\n\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-asciidoc.vue",
    "content": "<template lang='pug'>\n  .editor-asciidoc\n    v-toolbar.editor-asciidoc-toolbar(dense, color='primary', dark, flat, style='overflow-x: hidden;')\n      template(v-if='isModalShown')\n        v-spacer\n        v-btn.animated.fadeInRight(text, @click='closeAllModal')\n          v-icon(left) mdi-arrow-left-circle\n          span {{$t('editor:backToEditor')}}\n      template(v-else)\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn(icon, tile, v-on='on', @click='toggleMarkup({ start: `**` })').mx-0\n              v-icon mdi-format-bold\n          span {{$t('editor:markup.bold')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='toggleMarkup({ start: `__` })').mx-0\n              v-icon mdi-format-italic\n          span {{$t('editor:markup.italic')}}\n        v-menu(offset-y, open-on-hover)\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p3s(icon, tile, v-on='on').mx-0\n              v-icon mdi-format-header-pound\n          v-list.py-0\n            template(v-for='(n, idx) in 6')\n              v-list-item(@click='setHeaderLine(n)', :key='idx')\n                v-list-item-action\n                  v-icon(:size='24 - (idx - 1) * 2') mdi-format-header-{{n}}\n                v-list-item-title {{$t('editor:markup.heading', { level: n })}}\n              v-divider(v-if='idx < 5')\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p4s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~` })').mx-0\n              v-icon mdi-format-subscript\n          span {{$t('editor:markup.subscript')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p5s(icon, tile, v-on='on', @click='toggleMarkup({ start: `^` })').mx-0\n              v-icon mdi-format-superscript\n          span {{$t('editor:markup.superscript')}}\n        v-menu(offset-y, open-on-hover)\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p6s(icon, tile, v-on='on').mx-0\n              v-icon mdi-alpha-t-box-outline\n          v-list.py-0\n            v-list-item(@click='insertBeforeEachLine({ content: `> `})')\n              v-list-item-action\n                v-icon mdi-alpha-t-box-outline\n              v-list-item-title {{$t('editor:markup.blockquote')}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `NOTE: `})')\n              v-list-item-action\n                v-icon(color='blue') mdi-alpha-n-box-outline\n              v-list-item-title {{'Note blockquote'}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `TIP: `})')\n              v-list-item-action\n                v-icon(color='success') mdi-alpha-t-box-outline\n              v-list-item-title {{'Tip blockquote'}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `WARNING: `})')\n              v-list-item-action\n                v-icon(color='warning') mdi-alpha-w-box-outline\n              v-list-item-title {{$t('editor:markup.blockquoteWarning')}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `CAUTION: `})')\n              v-list-item-action\n                v-icon(color='purple') mdi-alpha-c-box-outline\n              v-list-item-title {{'Caution blockquote'}}\n            v-list-item(@click='insertBeforeEachLine({ content: `IMPORTANT: `})')\n              v-list-item-action\n                v-icon(color='error') mdi-alpha-i-box-outline\n              v-list-item-title {{'Important blockquote'}}\n            v-divider\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          v-tooltip(bottom, color='primary')\n            template(v-slot:activator='{ on }')\n              v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='previewShown = !previewShown').mx-0\n                v-icon mdi-book-open-outline\n            span {{$t('editor:markup.togglePreviewPane')}}\n\n    .editor-asciidoc-main\n      .editor-asciidoc-sidebar\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, @click='insertLink').mx-0\n              v-icon mdi-link-plus\n          span {{$t('editor:markup.insertLink')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0\n              v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image\n          span {{$t('editor:markup.insertAssets')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0\n              v-icon mdi-chart-multiline\n          span {{$t('editor:markup.insertDiagram')}}\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          v-tooltip(right, color='teal')\n            template(v-slot:activator='{ on }')\n              v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0\n                v-icon mdi-arrow-expand-all\n            span {{$t('editor:markup.distractionFreeMode')}}\n      .editor-asciidoc-editor\n        textarea(ref='cm')\n      transition(name='editor-asciidoc-preview')\n        .editor-asciidoc-preview(v-if='previewShown')\n          .editor-asciidoc-preview-content.contents(ref='editorPreviewContainer')\n            div(\n              ref='editorPreview'\n              v-html='previewHTML'\n              )\n\n    v-system-bar.editor-asciidoc-sysbar(dark, status, color='grey darken-3')\n      .caption.editor-asciidoc-sysbar-locale {{locale.toUpperCase()}}\n      .caption.px-3 /{{path}}\n      template(v-if='$vuetify.breakpoint.mdAndUp')\n        v-spacer\n        .caption AsciiDoc\n        v-spacer\n        .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}\n    page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get, sync } from 'vuex-pathify'\nimport DOMPurify from 'dompurify'\n\n// ========================================\n// IMPORTS\n// ========================================\n\n// Code Mirror\nimport CodeMirror from 'codemirror'\nimport 'codemirror/lib/codemirror.css'\n\n// Language\nimport 'codemirror-asciidoc'\n\n// Addons\nimport 'codemirror/addon/selection/active-line.js'\nimport 'codemirror/addon/display/fullscreen.js'\nimport 'codemirror/addon/display/fullscreen.css'\nimport 'codemirror/addon/selection/mark-selection.js'\nimport 'codemirror/addon/search/searchcursor.js'\nimport 'codemirror/addon/hint/show-hint.js'\nimport 'codemirror/addon/fold/foldcode.js'\nimport 'codemirror/addon/fold/foldgutter.js'\nimport 'codemirror/addon/fold/foldgutter.css'\nimport cmFold from './common/cmFold'\n\n// ========================================\n// INIT\n// ========================================\nconst asciidoctor = require('asciidoctor')()\nconst cheerio = require('cheerio')\n\n// Platform detection\nconst CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'\n\n// ========================================\n// HELPER FUNCTIONS\n// ========================================\n\ncmFold.register('asciidoc')\n\n// ========================================\n// Vue Component\n// ========================================\n\nexport default {\n  data() {\n    return {\n      cm: null,\n      cursorPos: { ch: 0, line: 1 },\n      previewShown: true, // TODO\n      insertLinkDialog: false,\n      helpShown: false,\n      previewHTML: ''\n    }\n  },\n  computed: {\n    isMobile() {\n      return this.$vuetify.breakpoint.smAndDown\n    },\n    isModalShown() {\n      return this.helpShown || this.activeModal !== ''\n    },\n    locale: get('page/locale'),\n    path: get('page/path'),\n    mode: get('editor/mode'),\n    activeModal: sync('editor/activeModal')\n  },\n\n  methods: {\n    toggleModal(key) {\n      this.activeModal = (this.activeModal === key) ? '' : key\n      this.helpShown = false\n    },\n    closeAllModal() {\n      this.activeModal = ''\n      this.helpShown = false\n    },\n    onCmInput: _.debounce(function(newContent) {\n      this.processContent(newContent)\n    }, 600),\n    processContent(newContent) {\n      this.processMarkers(this.cm.firstLine(), this.cm.lastLine())\n      let html = asciidoctor.convert(newContent, {\n        standalone: false,\n        safe: 'safe',\n        attributes: {\n          showtitle: true,\n          icons: 'font'\n        }\n      })\n      const $ = cheerio.load(html, {\n        decodeEntities: true\n      })\n\n      $('pre.highlight > code.language-diagram').each((i, elm) => {\n        const diagramContent = Buffer.from($(elm).html(), 'base64').toString()\n        $(elm).parent().replaceWith(`<pre class=\"diagram\">${diagramContent}</div>`)\n      })\n\n      this.previewHTML = DOMPurify.sanitize($.html(), {\n        ADD_TAGS: ['foreignObject'],\n        HTML_INTEGRATION_POINTS: { foreignobject: true }\n      })\n    },\n    /**\n     * Insert content at cursor\n     */\n    insertAtCursor({ content }) {\n      const cursor = this.cm.doc.getCursor('head')\n      this.cm.doc.replaceRange(content, cursor)\n    },\n    /**\n     * Insert content after current line\n     */\n    insertAfter({ content, newLine }) {\n      const curLine = this.cm.doc.getCursor('to').line\n      const lineLength = this.cm.doc.getLine(curLine).length\n      this.cm.doc.replaceRange(newLine ? `\\n${content}\\n` : content, { line: curLine, ch: lineLength + 1 })\n    },\n    /**\n     * Insert content before current line\n     */\n    insertBeforeEachLine({ content, after }) {\n      let lines = []\n      if (!this.cm.doc.somethingSelected()) {\n        lines.push(this.cm.doc.getCursor('head').line)\n      } else {\n        lines = _.flatten(this.cm.doc.listSelections().map(sl => {\n          const range = Math.abs(sl.anchor.line - sl.head.line) + 1\n          const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line\n          return _.times(range, l => l + lowestLine)\n        }))\n      }\n      lines.forEach(ln => {\n        let lineContent = this.cm.doc.getLine(ln)\n        const lineLength = lineContent.length\n        if (_.startsWith(lineContent, content)) {\n          lineContent = lineContent.substring(content.length)\n        }\n\n        this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })\n      })\n      if (after) {\n        const lastLine = _.last(lines)\n        this.cm.doc.replaceRange(`\\n${after}\\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })\n      }\n    },\n    /**\n     * Update cursor state\n     */\n    positionSync(cm) {\n      this.cursorPos = cm.getCursor('head')\n    },\n    toggleMarkup({ start, end }) {\n      if (!end) { end = start }\n      if (!this.cm.doc.somethingSelected()) {\n        return this.$store.commit('showNotification', {\n          message: this.$t('editor:markup.noSelectionError'),\n          style: 'warning',\n          icon: 'warning'\n        })\n      }\n      this.cm.doc.replaceSelections(this.cm.doc.getSelections().map(s => start + s + end))\n    },\n    setHeaderLine(lvl) {\n      const curLine = this.cm.doc.getCursor('head').line\n      let lineContent = this.cm.doc.getLine(curLine)\n      const lineLength = lineContent.length\n      if (_.startsWith(lineContent, '=')) {\n        lineContent = lineContent.replace(/^(=+ )/, '')\n      }\n      lineContent = _.times(lvl, n => '=').join('') + ` ` + lineContent\n      this.cm.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength })\n    },\n\n    toggleFullscreen () {\n      this.cm.setOption('fullScreen', true)\n    },\n    refresh() {\n      this.$nextTick(() => {\n        this.cm.refresh()\n      })\n    },\n    insertLink () {\n      this.insertLinkDialog = true\n    },\n    insertLinkHandler ({ locale, path }) {\n      const lastPart = _.last(path.split('/'))\n      this.insertAtCursor({\n        content: siteLangs.length > 0 ? `link:/${locale}/${path}[${lastPart}]` : `link:/${path}[${lastPart}]`\n      })\n    },\n    processMarkers (from, to) {\n      let found = null\n      let foundStart = 0\n      this.cm.doc.getAllMarks().forEach(mk => {\n        if (mk.__kind) {\n          mk.clear()\n        }\n      })\n      this.cm.eachLine(from, to, ln => {\n        const line = ln.lineNo()\n        if (ln.text.startsWith('```diagram')) {\n          found = 'diagram'\n          foundStart = line\n        } else if (ln.text === '```' && found) {\n          switch (found) {\n            // ------------------------------\n            // -> DIAGRAM\n            // ------------------------------\n            case 'diagram': {\n              if (line - foundStart !== 2) {\n                return\n              }\n              this.addMarker({\n                kind: 'diagram',\n                from: { line: foundStart, ch: 3 },\n                to: { line: foundStart, ch: 10 },\n                text: 'Edit Diagram',\n                action: ((start, end) => {\n                  return (ev) => {\n                    this.cm.doc.setSelection({ line: start, ch: 0 }, { line: end, ch: 3 })\n                    try {\n                      const raw = this.cm.doc.getLine(end - 1)\n                      this.$store.set('editor/activeModalData', Buffer.from(raw, 'base64').toString())\n                      this.toggleModal(`editorModalDrawio`)\n                    } catch (err) {\n                      return this.$store.commit('showNotification', {\n                        message: 'Failed to process diagram data.',\n                        style: 'warning',\n                        icon: 'warning'\n                      })\n                    }\n                  }\n                })(foundStart, line)\n              })\n              if (ln.height > 0) {\n                this.cm.foldCode(foundStart)\n              }\n              break\n            }\n          }\n          found = null\n        }\n      })\n    },\n    addMarker ({ kind, from, to, text, action }) {\n      const markerElm = document.createElement('span')\n      markerElm.appendChild(document.createTextNode(text))\n      markerElm.className = 'CodeMirror-buttonmarker'\n      markerElm.addEventListener('click', action)\n      this.cm.markText(from, to, { replacedWith: markerElm, __kind: kind })\n    }\n  },\n  mounted() {\n    this.$store.set('editor/editorKey', 'asciidoc')\n\n    if (this.mode === 'create') {\n      this.$store.set('editor/content', '== header\\n\\ncontent')\n    }\n\n    // Initialize CodeMirror\n\n    this.cm = CodeMirror.fromTextArea(this.$refs.cm, {\n      tabSize: 2,\n      mode: 'asciidoc',\n      theme: 'wikijs-dark',\n      lineNumbers: true,\n      lineWrapping: true,\n      line: true,\n      styleActiveLine: true,\n      highlightSelectionMatches: {\n        annotateScrollbar: true\n      },\n      viewportMargin: 50,\n      inputStyle: 'contenteditable',\n      allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],\n      direction: siteConfig.rtl ? 'rtl' : 'ltr',\n      foldGutter: true,\n      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']\n\n    })\n    this.cm.setValue(this.$store.get('editor/content'))\n    this.cm.on('change', c => {\n      this.$store.set('editor/content', c.getValue())\n      this.onCmInput(this.$store.get('editor/content'))\n    })\n    if (this.$vuetify.breakpoint.mdAndUp) {\n      this.cm.setSize(null, 'calc(100vh - 137px)')\n    } else {\n      this.cm.setSize(null, 'calc(100vh - 112px - 16px)')\n    }\n\n    // Set Keybindings\n\n    const keyBindings = {\n      'F11' (c) {\n        c.setOption('fullScreen', !c.getOption('fullScreen'))\n      },\n      'Esc' (c) {\n        if (c.getOption('fullScreen')) c.setOption('fullScreen', false)\n      }\n    }\n    _.set(keyBindings, `${CtrlKey}-B`, c => {\n      this.toggleMarkup({ start: `**` })\n      return false\n    })\n    _.set(keyBindings, `${CtrlKey}-I`, c => {\n      this.toggleMarkup({ start: `__` })\n      return false\n    })\n\n    this.cm.setOption('extraKeys', keyBindings)\n\n    // Handle cursor movement\n\n    this.cm.on('cursorActivity', c => {\n      this.positionSync(c)\n    })\n\n    // Render initial preview\n    this.processContent(this.$store.get('editor/content'))\n\n    this.$root.$on('editorInsert', opts => {\n      switch (opts.kind) {\n        case 'IMAGE':\n          let img = `image::${opts.path}[${opts.text}]`\n          this.insertAtCursor({\n            content: img\n          })\n          break\n        case 'BINARY':\n          this.insertAtCursor({\n            content: `link:${opts.path}[${opts.text}]`\n          })\n          break\n        case 'DIAGRAM':\n          const selStartLine = this.cm.getCursor('from').line\n          const selEndLine = this.cm.getCursor('to').line + 1\n          this.cm.doc.replaceSelection('```diagram\\n' + opts.text + '\\n```\\n', 'start')\n          this.processMarkers(selStartLine, selEndLine)\n          break\n      }\n    })\n\n    // Handle save conflict\n    this.$root.$on('saveConflict', () => {\n      this.toggleModal(`editorModalConflict`)\n    })\n    this.$root.$on('overwriteEditorContent', () => {\n      this.cm.setValue(this.$store.get('editor/content'))\n    })\n  },\n  beforeDestroy() {\n    this.$root.$off('editorInsert')\n  }\n}\n</script>\n\n<style lang='scss'>\n$editor-ascii-height: calc(100vh - 137px);\n$editor-ascii-height-mobile: calc(100vh - 112px - 16px);\n\n.editor-asciidoc {\n  &-main {\n    display: flex;\n    width: 100%;\n  }\n\n  &-editor {\n    background-color: darken(mc('grey', '900'), 4.5%);\n    flex: 1 1 50%;\n    display: block;\n    height: $editor-ascii-height;\n    position: relative;\n\n    @include until($tablet) {\n      height: $editor-ascii-height-mobile;\n    }\n  }\n\n  &-preview {\n    flex: 1 1 50%;\n    background-color: mc('grey', '100');\n    position: relative;\n    height: $editor-ascii-height;\n    overflow: hidden;\n    padding: 1rem;\n\n    @at-root .theme--dark & {\n      background-color: mc('grey', '900');\n    }\n\n    @include until($tablet) {\n      display: none;\n    }\n\n    &-enter-active, &-leave-active {\n      transition: max-width .5s ease;\n      max-width: 50vw;\n\n      .editor-code-preview-content {\n        width: 50vw;\n        overflow:hidden;\n      }\n    }\n    &-enter, &-leave-to {\n      max-width: 0;\n    }\n\n    &-content {\n      height: $editor-ascii-height;\n      overflow-y: scroll;\n      padding: 0;\n      width: calc(100% + 17px);\n      // -ms-overflow-style: none;\n\n      // &::-webkit-scrollbar {\n      //   width: 0px;\n      //   background: transparent;\n      // }\n\n      @include until($tablet) {\n        height: $editor-ascii-height-mobile;\n      }\n\n      > div {\n        outline: none;\n      }\n\n      p.line {\n        overflow-wrap: break-word;\n      }\n\n      .tabset {\n        background-color: mc('teal', '700');\n        color: mc('teal', '100') !important;\n        padding: 5px 12px;\n        font-size: 14px;\n        font-weight: 500;\n        border-radius: 5px 0 0 0;\n        font-style: italic;\n\n        &::after {\n          display: none;\n        }\n\n        &-header {\n          background-color: mc('teal', '500');\n          color: #FFF !important;\n          padding: 5px 12px;\n          font-size: 14px;\n          font-weight: 500;\n          margin-top: 0 !important;\n\n          &::after {\n            display: none;\n          }\n        }\n\n        &-content {\n          border-left: 5px solid mc('teal', '500');\n          background-color: mc('teal', '50');\n          padding: 0 15px 15px;\n          overflow: hidden;\n\n          @at-root .theme--dark & {\n            background-color: rgba(mc('teal', '500'), .1);\n          }\n        }\n      }\n    }\n  }\n\n  &-toolbar {\n    background-color: mc('blue', '700');\n    background-image: linear-gradient(to bottom, mc('blue', '700') 0%, mc('blue','800') 100%);\n    color: #FFF;\n\n    .v-toolbar__content {\n      padding-left: 64px;\n\n      @include until($tablet) {\n        padding-left: 8px;\n      }\n    }\n  }\n\n  &-insert:not(.v-speed-dial--right) {\n    @include from($tablet) {\n      left: 50%;\n      margin-left: -28px;\n    }\n  }\n\n  &-sidebar {\n    background-color: mc('grey', '900');\n    width: 64px;\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-start;\n    align-items: center;\n    padding: 24px 0;\n\n    @include until($tablet) {\n      padding: 12px 0;\n      width: 40px;\n    }\n  }\n\n  &-sysbar {\n    padding-left: 0;\n\n    &-locale {\n      background-color: rgba(255,255,255,.25);\n      display:inline-flex;\n      padding: 0 12px;\n      height: 24px;\n      width: 63px;\n      justify-content: center;\n      align-items: center;\n    }\n  }\n\n  // ==========================================\n  // Fix FAB revealing under codemirror\n  // ==========================================\n\n  .speed-dial--fixed {\n    z-index: 8;\n  }\n\n  // ==========================================\n  // CODE MIRROR\n  // ==========================================\n\n  .CodeMirror {\n    height: auto;\n    font-family: 'Roboto Mono', monospace;\n    font-size: .9rem;\n\n    .cm-header-1 {\n      font-size: 1.5rem;\n    }\n    .cm-header-2 {\n      font-size: 1.25rem;\n    }\n    .cm-header-3 {\n      font-size: 1.15rem;\n    }\n    .cm-header-4 {\n      font-size: 1.1rem;\n    }\n    .cm-header-5 {\n      font-size: 1.05rem;\n    }\n    .cm-header-6 {\n      font-size: 1.025rem;\n    }\n  }\n\n  .CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like {\n    word-break: break-word;\n  }\n\n  .CodeMirror-focused .cm-matchhighlight {\n    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);\n    background-position: bottom;\n    background-repeat: repeat-x;\n  }\n  .cm-matchhighlight {\n    background-color: mc('grey', '800');\n  }\n  .CodeMirror-selection-highlight-scrollbar {\n    background-color: mc('green', '600');\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-ckeditor.vue",
    "content": "<template lang='pug'>\n  .editor-ckeditor\n    div(ref='toolbarContainer')\n    div.contents(ref='editor')\n    v-system-bar.editor-ckeditor-sysbar(dark, status, color='grey darken-3')\n      .caption.editor-ckeditor-sysbar-locale {{locale.toUpperCase()}}\n      .caption.px-3 /{{path}}\n      template(v-if='$vuetify.breakpoint.mdAndUp')\n        v-spacer\n        .caption Visual Editor\n        v-spacer\n        .caption {{$t('editor:ckeditor.stats', { chars: stats.characters, words: stats.words })}}\n    editor-conflict(v-model='isConflict', v-if='isConflict')\n    page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get, sync } from 'vuex-pathify'\nimport DecoupledEditor from '@requarks/ckeditor5'\n// import DecoupledEditor from '../../../../wiki-ckeditor5/build/ckeditor'\nimport EditorConflict from './ckeditor/conflict.vue'\nimport { html as beautify } from 'js-beautify/js/lib/beautifier.min.js'\n\n/* global siteLangs */\n\nexport default {\n  components: {\n    EditorConflict\n  },\n  props: {\n    save: {\n      type: Function,\n      default: () => {}\n    }\n  },\n  data() {\n    return {\n      editor: null,\n      stats: {\n        characters: 0,\n        words: 0\n      },\n      content: '',\n      isConflict: false,\n      insertLinkDialog: false\n    }\n  },\n  computed: {\n    isMobile() {\n      return this.$vuetify.breakpoint.smAndDown\n    },\n    locale: get('page/locale'),\n    path: get('page/path'),\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n    insertLink () {\n      this.insertLinkDialog = true\n    },\n    insertLinkHandler ({ locale, path }) {\n      this.editor.execute('link', siteLangs.length > 0 ? `/${locale}/${path}` : `/${path}`)\n    }\n  },\n  async mounted () {\n    this.$store.set('editor/editorKey', 'ckeditor')\n\n    this.editor = await DecoupledEditor.create(this.$refs.editor, {\n      language: this.locale,\n      placeholder: 'Type the page content here',\n      disableNativeSpellChecker: false,\n      // TODO: Mention autocomplete\n      //\n      // mention: {\n      //   feeds: [\n      //     {\n      //       marker: '@',\n      //       feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ],\n      //       minimumCharacters: 1\n      //     }\n      //   ]\n      // },\n      wordCount: {\n        onUpdate: stats => {\n          this.stats = {\n            characters: stats.characters,\n            words: stats.words\n          }\n        }\n      }\n    })\n    this.$refs.toolbarContainer.appendChild(this.editor.ui.view.toolbar.element)\n\n    if (this.mode !== 'create') {\n      this.editor.setData(this.$store.get('editor/content'))\n    }\n\n    this.editor.model.document.on('change:data', _.debounce(evt => {\n      this.$store.set('editor/content', beautify(this.editor.getData(), { indent_size: 2, end_with_newline: true }))\n    }, 300))\n\n    this.$root.$on('editorInsert', opts => {\n      switch (opts.kind) {\n        case 'IMAGE':\n          this.editor.execute('imageInsert', {\n            source: opts.path\n          })\n          break\n        case 'BINARY':\n          this.editor.execute('link', opts.path, {\n            linkIsDownloadable: true\n          })\n          break\n        case 'DIAGRAM':\n          this.editor.execute('imageInsert', {\n            source: `data:image/svg+xml;base64,${opts.text}`\n          })\n          break\n      }\n    })\n\n    this.$root.$on('editorLinkToPage', opts => {\n      this.insertLink()\n    })\n\n    // Handle save conflict\n    this.$root.$on('saveConflict', () => {\n      this.isConflict = true\n    })\n    this.$root.$on('overwriteEditorContent', () => {\n      this.editor.setData(this.$store.get('editor/content'))\n    })\n  },\n  beforeDestroy () {\n    if (this.editor) {\n      this.editor.destroy()\n      this.editor = null\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n\n$editor-height: calc(100vh - 64px - 24px);\n$editor-height-mobile: calc(100vh - 56px - 16px);\n\n.editor-ckeditor {\n  background-color: mc('grey', '200');\n  flex: 1 1 50%;\n  display: flex;\n  flex-flow: column nowrap;\n  height: $editor-height;\n  max-height: $editor-height;\n  position: relative;\n\n  @at-root .theme--dark & {\n    background-color: mc('grey', '900');\n  }\n\n  @include until($tablet) {\n    height: $editor-height-mobile;\n    max-height: $editor-height-mobile;\n  }\n\n  &-sysbar {\n    padding-left: 0;\n\n    &-locale {\n      background-color: rgba(255,255,255,.25);\n      display:inline-flex;\n      padding: 0 12px;\n      height: 24px;\n      width: 63px;\n      justify-content: center;\n      align-items: center;\n    }\n  }\n\n  .contents {\n    table {\n      margin: inherit;\n    }\n    pre > code {\n      background-color: unset;\n      color: unset;\n      padding: .15em;\n    }\n  }\n\n  .ck.ck-toolbar {\n    border: none;\n    justify-content: center;\n    background-color: mc('grey', '300');\n    color: #FFF;\n  }\n\n  .ck.ck-toolbar__items {\n    justify-content: center;\n  }\n\n  > .ck-editor__editable {\n    background-color: mc('grey', '100');\n    overflow-y: auto;\n    overflow-x: hidden;\n    padding: 2rem;\n    box-shadow: 0 0 5px hsla(0, 0, 0, .1);\n    margin: 1rem auto 0;\n    width: calc(100vw - 256px - 16vw);\n    min-height: calc(100vh - 64px - 24px - 1rem - 40px);\n    border-radius: 5px;\n\n    @at-root .theme--dark & {\n      background-color: #303030;\n      color: #FFF;\n    }\n\n    @include until($widescreen) {\n      width: calc(100vw - 2rem);\n      margin: 1rem 1rem 0 1rem;\n      min-height: calc(100vh - 64px - 24px - 1rem - 40px);\n    }\n\n    @include until($tablet) {\n      width: 100%;\n      margin: 0;\n      min-height: calc(100vh - 56px - 24px - 76px);\n    }\n\n    &.ck.ck-editor__editable:not(.ck-editor__nested-editable).ck-focused {\n      border-color: #FFF;\n      box-shadow: 0 0 10px rgba(mc('blue', '700'), .25);\n\n      @at-root .theme--dark & {\n        border-color: #444;\n        border-bottom: none;\n        box-shadow: 0 0 10px rgba(#000, .25);\n      }\n    }\n\n    &.ck .ck-editor__nested-editable.ck-editor__nested-editable_focused,\n    &.ck .ck-editor__nested-editable:focus,\n    .ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused,\n    .ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused {\n      background-color: mc('grey', '100');\n\n      @at-root .theme--dark & {\n        background-color: mc('grey', '900');\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-code.vue",
    "content": "<template lang='pug'>\n  .editor-code\n    .editor-code-main\n      .editor-code-sidebar\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, disabled).mx-0\n              v-icon mdi-link-plus\n          span {{$t('editor:markup.insertLink')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0\n              v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image\n          span {{$t('editor:markup.insertAssets')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalBlocks`)', disabled).mx-0\n              v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') mdi-view-dashboard-outline\n          span {{$t('editor:markup.insertBlock')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, disabled).mx-0\n              v-icon mdi-code-braces\n          span {{$t('editor:markup.insertCodeBlock')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0\n              v-icon mdi-library-video\n          span {{$t('editor:markup.insertVideoAudio')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, disabled).mx-0\n              v-icon mdi-chart-multiline\n          span {{$t('editor:markup.insertDiagram')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p6s(icon, tile, v-on='on', dark, disabled).mx-0\n              v-icon mdi-function-variant\n          span {{$t('editor:markup.insertMathExpression')}}\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          v-tooltip(right, color='teal')\n            template(v-slot:activator='{ on }')\n              v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0\n                v-icon mdi-arrow-expand-all\n            span {{$t('editor:markup.distractionFreeMode')}}\n      .editor-code-editor\n        textarea(ref='cm')\n    v-system-bar.editor-code-sysbar(dark, status, color='grey darken-3')\n      .caption.editor-code-sysbar-locale {{locale.toUpperCase()}}\n      .caption.px-3 /{{path}}\n      template(v-if='$vuetify.breakpoint.mdAndUp')\n        v-spacer\n        .caption Code\n        v-spacer\n        .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get, sync } from 'vuex-pathify'\n\n// ========================================\n// IMPORTS\n// ========================================\n\n// Code Mirror\nimport CodeMirror from 'codemirror'\nimport 'codemirror/lib/codemirror.css'\n\n// Language\nimport 'codemirror/mode/htmlmixed/htmlmixed.js'\n\n// Addons\nimport 'codemirror/addon/selection/active-line.js'\nimport 'codemirror/addon/display/fullscreen.js'\nimport 'codemirror/addon/display/fullscreen.css'\nimport 'codemirror/addon/selection/mark-selection.js'\nimport 'codemirror/addon/search/searchcursor.js'\n\n// ========================================\n// INIT\n// ========================================\n\n// Platform detection\n// const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'\n\n// ========================================\n// Vue Component\n// ========================================\n\nexport default {\n  data() {\n    return {\n      cm: null,\n      cursorPos: { ch: 0, line: 1 }\n    }\n  },\n  computed: {\n    isMobile() {\n      return this.$vuetify.breakpoint.smAndDown\n    },\n    locale: get('page/locale'),\n    path: get('page/path'),\n    mode: get('editor/mode'),\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n    toggleModal(key) {\n      this.activeModal = (this.activeModal === key) ? '' : key\n      this.helpShown = false\n    },\n    closeAllModal() {\n      this.activeModal = ''\n      this.helpShown = false\n    },\n    /**\n     * Insert content at cursor\n     */\n    insertAtCursor({ content }) {\n      const cursor = this.cm.doc.getCursor('head')\n      this.cm.doc.replaceRange(content, cursor)\n    },\n    /**\n     * Insert content after current line\n     */\n    insertAfter({ content, newLine }) {\n      const curLine = this.cm.doc.getCursor('to').line\n      const lineLength = this.cm.doc.getLine(curLine).length\n      this.cm.doc.replaceRange(newLine ? `\\n${content}\\n` : content, { line: curLine, ch: lineLength + 1 })\n    },\n    /**\n     * Insert content before current line\n     */\n    insertBeforeEachLine({ content, after }) {\n      let lines = []\n      if (!this.cm.doc.somethingSelected()) {\n        lines.push(this.cm.doc.getCursor('head').line)\n      } else {\n        lines = _.flatten(this.cm.doc.listSelections().map(sl => {\n          const range = Math.abs(sl.anchor.line - sl.head.line) + 1\n          const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line\n          return _.times(range, l => l + lowestLine)\n        }))\n      }\n      lines.forEach(ln => {\n        let lineContent = this.cm.doc.getLine(ln)\n        const lineLength = lineContent.length\n        if (_.startsWith(lineContent, content)) {\n          lineContent = lineContent.substring(content.length)\n        }\n\n        this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })\n      })\n      if (after) {\n        const lastLine = _.last(lines)\n        this.cm.doc.replaceRange(`\\n${after}\\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })\n      }\n    },\n    /**\n     * Update cursor state\n     */\n    positionSync(cm) {\n      this.cursorPos = cm.getCursor('head')\n    },\n    toggleFullscreen () {\n      this.cm.setOption('fullScreen', true)\n    },\n    refresh() {\n      this.$nextTick(() => {\n        this.cm.refresh()\n      })\n    }\n  },\n  mounted() {\n    this.$store.set('editor/editorKey', 'code')\n\n    if (this.mode === 'create') {\n      this.$store.set('editor/content', '<h1>Title</h1>\\n\\n<p>Some text here</p>')\n    }\n\n    // Initialize CodeMirror\n\n    this.cm = CodeMirror.fromTextArea(this.$refs.cm, {\n      tabSize: 2,\n      mode: 'text/html',\n      theme: 'wikijs-dark',\n      lineNumbers: true,\n      lineWrapping: true,\n      line: true,\n      styleActiveLine: true,\n      highlightSelectionMatches: {\n        annotateScrollbar: true\n      },\n      viewportMargin: 50,\n      inputStyle: 'contenteditable',\n      allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif']\n    })\n    this.cm.setValue(this.$store.get('editor/content'))\n    this.cm.on('change', c => {\n      this.$store.set('editor/content', c.getValue())\n    })\n    if (this.$vuetify.breakpoint.mdAndUp) {\n      this.cm.setSize(null, 'calc(100vh - 64px - 24px)')\n    } else {\n      this.cm.setSize(null, 'calc(100vh - 56px - 16px)')\n    }\n\n    // Set Keybindings\n\n    const keyBindings = {\n      'F11' (c) {\n        c.setOption('fullScreen', !c.getOption('fullScreen'))\n      },\n      'Esc' (c) {\n        if (c.getOption('fullScreen')) c.setOption('fullScreen', false)\n      }\n    }\n    this.cm.setOption('extraKeys', keyBindings)\n\n    // Handle cursor movement\n\n    this.cm.on('cursorActivity', c => {\n      this.positionSync(c)\n    })\n\n    // Render initial preview\n\n    this.$root.$on('editorInsert', opts => {\n      switch (opts.kind) {\n        case 'IMAGE':\n          let img = `<img src=\"${opts.path}\" alt=\"${opts.text}\"`\n          if (opts.align && opts.align !== '') {\n            img += ` class=\"align-${opts.align}\"`\n          }\n          img += ` />`\n          this.insertAtCursor({\n            content: img\n          })\n          break\n        case 'BINARY':\n          this.insertAtCursor({\n            content: `<a href=\"${opts.path}\" title=\"${opts.text}\">${opts.text}</a>`\n          })\n          break\n      }\n    })\n\n    // Handle save conflict\n    this.$root.$on('saveConflict', () => {\n      this.toggleModal(`editorModalConflict`)\n    })\n    this.$root.$on('overwriteEditorContent', () => {\n      this.cm.setValue(this.$store.get('editor/content'))\n    })\n  },\n  beforeDestroy() {\n    this.$root.$off('editorInsert')\n  }\n}\n</script>\n\n<style lang='scss'>\n$editor-height: calc(100vh - 64px - 24px);\n$editor-height-mobile: calc(100vh - 56px - 16px);\n\n.editor-code {\n  &-main {\n    display: flex;\n    width: 100%;\n  }\n\n  &-editor {\n    background-color: darken(mc('grey', '900'), 4.5%);\n    flex: 1 1 50%;\n    display: block;\n    height: $editor-height;\n    position: relative;\n\n    &-title {\n      background-color: mc('grey', '800');\n      border-bottom-left-radius: 5px;\n      display: inline-flex;\n      height: 30px;\n      justify-content: center;\n      align-items: center;\n      padding: 0 1rem;\n      color: mc('grey', '500');\n      position: absolute;\n      top: 0;\n      right: 0;\n      z-index: 7;\n      text-transform: uppercase;\n      font-size: .7rem;\n      cursor: pointer;\n\n      @include until($tablet) {\n        display: none;\n      }\n    }\n  }\n\n  &-sidebar {\n    background-color: mc('grey', '900');\n    width: 64px;\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-start;\n    align-items: center;\n    padding: 24px 0;\n\n    @include until($tablet) {\n      padding: 12px 0;\n      width: 40px;\n    }\n  }\n\n  &-sysbar {\n    padding-left: 0;\n\n    &-locale {\n      background-color: rgba(255,255,255,.25);\n      display:inline-flex;\n      padding: 0 12px;\n      height: 24px;\n      width: 63px;\n      justify-content: center;\n      align-items: center;\n    }\n  }\n\n  // ==========================================\n  // CODE MIRROR\n  // ==========================================\n\n  .CodeMirror {\n    height: auto;\n\n    .cm-header-1 {\n      font-size: 1.5rem;\n    }\n    .cm-header-2 {\n      font-size: 1.25rem;\n    }\n    .cm-header-3 {\n      font-size: 1.15rem;\n    }\n    .cm-header-4 {\n      font-size: 1.1rem;\n    }\n    .cm-header-5 {\n      font-size: 1.05rem;\n    }\n    .cm-header-6 {\n      font-size: 1.025rem;\n    }\n  }\n\n  .CodeMirror-focused .cm-matchhighlight {\n    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);\n    background-position: bottom;\n    background-repeat: repeat-x;\n  }\n  .cm-matchhighlight {\n    background-color: mc('grey', '800');\n  }\n  .CodeMirror-selection-highlight-scrollbar {\n    background-color: mc('green', '600');\n  }\n\n  .cm-s-wikijs-dark.CodeMirror {\n    background: darken(mc('grey','900'), 3%);\n    color: #e0e0e0;\n  }\n  .cm-s-wikijs-dark div.CodeMirror-selected {\n    background: mc('blue','800');\n  }\n  .cm-s-wikijs-dark .cm-matchhighlight {\n    background: mc('blue','800');\n  }\n  .cm-s-wikijs-dark .CodeMirror-line::selection, .cm-s-wikijs-dark .CodeMirror-line > span::selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::selection {\n    background: mc('amber', '500');\n  }\n  .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 {\n    background: mc('amber', '500');\n  }\n  .cm-s-wikijs-dark .CodeMirror-gutters {\n    background: darken(mc('grey','900'), 6%);\n    border-right: 1px solid mc('grey','900');\n  }\n  .cm-s-wikijs-dark .CodeMirror-guttermarker {\n    color: #ac4142;\n  }\n  .cm-s-wikijs-dark .CodeMirror-guttermarker-subtle {\n    color: #505050;\n  }\n  .cm-s-wikijs-dark .CodeMirror-linenumber {\n    color: mc('grey','800');\n  }\n  .cm-s-wikijs-dark .CodeMirror-cursor {\n    border-left: 1px solid #b0b0b0;\n  }\n  .cm-s-wikijs-dark span.cm-comment {\n    color: mc('orange','800');\n  }\n  .cm-s-wikijs-dark span.cm-atom {\n    color: #aa759f;\n  }\n  .cm-s-wikijs-dark span.cm-number {\n    color: #aa759f;\n  }\n  .cm-s-wikijs-dark span.cm-property, .cm-s-wikijs-dark span.cm-attribute {\n    color: #90a959;\n  }\n  .cm-s-wikijs-dark span.cm-keyword {\n    color: #ac4142;\n  }\n  .cm-s-wikijs-dark span.cm-string {\n    color: #f4bf75;\n  }\n  .cm-s-wikijs-dark span.cm-variable {\n    color: #90a959;\n  }\n  .cm-s-wikijs-dark span.cm-variable-2 {\n    color: #6a9fb5;\n  }\n  .cm-s-wikijs-dark span.cm-def {\n    color: #d28445;\n  }\n  .cm-s-wikijs-dark span.cm-bracket {\n    color: #e0e0e0;\n  }\n  .cm-s-wikijs-dark span.cm-tag {\n    color: #ac4142;\n  }\n  .cm-s-wikijs-dark span.cm-link {\n    color: #aa759f;\n  }\n  .cm-s-wikijs-dark span.cm-error {\n    background: #ac4142;\n    color: #b0b0b0;\n  }\n  .cm-s-wikijs-dark .CodeMirror-activeline-background {\n    background: mc('grey','900');\n  }\n  .cm-s-wikijs-dark .CodeMirror-matchingbracket {\n    text-decoration: underline;\n    color: white !important;\n  }\n\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-markdown.vue",
    "content": "<template lang='pug'>\n  .editor-markdown\n    v-toolbar.editor-markdown-toolbar(dense, color='primary', dark, flat, style='overflow-x: hidden;')\n      template(v-if='isModalShown')\n        v-spacer\n        v-btn.animated.fadeInRight(text, @click='closeAllModal')\n          v-icon(left) mdi-arrow-left-circle\n          span {{$t('editor:backToEditor')}}\n      template(v-else)\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn(icon, tile, v-on='on', @click='toggleMarkup({ start: `**` })').mx-0\n              v-icon mdi-format-bold\n          span {{$t('editor:markup.bold')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='toggleMarkup({ start: `*` })').mx-0\n              v-icon mdi-format-italic\n          span {{$t('editor:markup.italic')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~~` })').mx-0\n              v-icon mdi-format-strikethrough\n          span {{$t('editor:markup.strikethrough')}}\n        v-menu(offset-y, open-on-hover)\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p3s(icon, tile, v-on='on').mx-0\n              v-icon mdi-format-header-pound\n          v-list.py-0\n            template(v-for='(n, idx) in 6')\n              v-list-item(@click='setHeaderLine(n)', :key='idx')\n                v-list-item-action\n                  v-icon(:size='24 - (idx - 1) * 2') mdi-format-header-{{n}}\n                v-list-item-title {{$t('editor:markup.heading', { level: n })}}\n              v-divider(v-if='idx < 5')\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p4s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~` })').mx-0\n              v-icon mdi-format-subscript\n          span {{$t('editor:markup.subscript')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p5s(icon, tile, v-on='on', @click='toggleMarkup({ start: `^` })').mx-0\n              v-icon mdi-format-superscript\n          span {{$t('editor:markup.superscript')}}\n        v-menu(offset-y, open-on-hover)\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p6s(icon, tile, v-on='on').mx-0\n              v-icon mdi-alpha-t-box-outline\n          v-list.py-0\n            v-list-item(@click='insertBeforeEachLine({ content: `> `})')\n              v-list-item-action\n                v-icon mdi-alpha-t-box-outline\n              v-list-item-title {{$t('editor:markup.blockquote')}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-info}`})')\n              v-list-item-action\n                v-icon(color='blue') mdi-alpha-i-box-outline\n              v-list-item-title {{$t('editor:markup.blockquoteInfo')}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-success}`})')\n              v-list-item-action\n                v-icon(color='success') mdi-alpha-s-box-outline\n              v-list-item-title {{$t('editor:markup.blockquoteSuccess')}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-warning}`})')\n              v-list-item-action\n                v-icon(color='warning') mdi-alpha-w-box-outline\n              v-list-item-title {{$t('editor:markup.blockquoteWarning')}}\n            v-divider\n            v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-danger}`})')\n              v-list-item-action\n                v-icon(color='error') mdi-alpha-e-box-outline\n              v-list-item-title {{$t('editor:markup.blockquoteError')}}\n            v-divider\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p7s(icon, tile, v-on='on', @click='insertBeforeEachLine({ content: `- `})').mx-0\n              v-icon mdi-format-list-bulleted\n          span {{$t('editor:markup.unorderedList')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p8s(icon, tile, v-on='on', @click='insertBeforeEachLine({ content: `1. `})').mx-0\n              v-icon mdi-format-list-numbered\n          span {{$t('editor:markup.orderedList')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p9s(icon, tile, v-on='on', @click='toggleMarkup({ start: \"`\" })').mx-0\n              v-icon mdi-code-tags\n          span {{$t('editor:markup.inlineCode')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p10s(icon, tile, v-on='on', @click='toggleMarkup({ start: `<kbd>`, end: `</kbd>` })').mx-0\n              v-icon mdi-keyboard-variant\n          span {{$t('editor:markup.keyboardKey')}}\n        v-tooltip(bottom, color='primary')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeIn.wait-p11s(icon, tile, v-on='on', @click='insertAfter({ content: `---`, newLine: true })').mx-0\n              v-icon mdi-minus\n          span {{$t('editor:markup.horizontalBar')}}\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          v-tooltip(bottom, color='primary', v-if='previewShown')\n            template(v-slot:activator='{ on }')\n              v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='spellModeActive = !spellModeActive').mx-0\n                v-icon(:color='spellModeActive ? `amber` : `white`') mdi-spellcheck\n            span {{$t('editor:markup.toggleSpellcheck')}}\n          v-tooltip(bottom, color='primary')\n            template(v-slot:activator='{ on }')\n              v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='previewShown = !previewShown').mx-0\n                v-icon mdi-book-open-outline\n            span {{$t('editor:markup.togglePreviewPane')}}\n    .editor-markdown-main\n      .editor-markdown-sidebar\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, @click='insertLink').mx-0\n              v-icon mdi-link-plus\n          span {{$t('editor:markup.insertLink')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0\n              v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image\n          span {{$t('editor:markup.insertAssets')}}\n        v-tooltip(right, color='teal')\n          template(v-slot:activator='{ on }')\n            v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0\n              v-icon mdi-chart-multiline\n          span {{$t('editor:markup.insertDiagram')}}\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          v-tooltip(right, color='teal')\n            template(v-slot:activator='{ on }')\n              v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0\n                v-icon mdi-arrow-expand-all\n            span {{$t('editor:markup.distractionFreeMode')}}\n          v-tooltip(right, color='teal')\n            template(v-slot:activator='{ on }')\n              v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0\n                v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle\n            span {{$t('editor:markup.markdownFormattingHelp')}}\n      .editor-markdown-editor\n        textarea(ref='cm')\n      transition(name='editor-markdown-preview')\n        .editor-markdown-preview(v-if='previewShown')\n          .editor-markdown-preview-content.contents(ref='editorPreviewContainer')\n            div(\n              ref='editorPreview'\n              v-html='previewHTML'\n              :spellcheck='spellModeActive'\n              :contenteditable='spellModeActive'\n              @blur='spellModeActive = false'\n              )\n\n    v-system-bar.editor-markdown-sysbar(dark, status, color='grey darken-3')\n      .caption.editor-markdown-sysbar-locale {{locale.toUpperCase()}}\n      .caption.px-3 /{{path}}\n      template(v-if='$vuetify.breakpoint.mdAndUp')\n        v-spacer\n        .caption Markdown\n        v-spacer\n        .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}\n\n    markdown-help(v-if='helpShown')\n    page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get, sync } from 'vuex-pathify'\nimport markdownHelp from './markdown/help.vue'\nimport gql from 'graphql-tag'\nimport DOMPurify from 'dompurify'\n\n/* global siteConfig, siteLangs */\n\n// ========================================\n// IMPORTS\n// ========================================\n\n// Code Mirror\nimport CodeMirror from 'codemirror'\nimport 'codemirror/lib/codemirror.css'\n\n// Language\nimport 'codemirror/mode/markdown/markdown.js'\n\n// Addons\nimport 'codemirror/addon/selection/active-line.js'\nimport 'codemirror/addon/display/fullscreen.js'\nimport 'codemirror/addon/display/fullscreen.css'\nimport 'codemirror/addon/selection/mark-selection.js'\nimport 'codemirror/addon/search/searchcursor.js'\nimport 'codemirror/addon/hint/show-hint.js'\nimport 'codemirror/addon/fold/foldcode.js'\nimport 'codemirror/addon/fold/foldgutter.js'\nimport 'codemirror/addon/fold/foldgutter.css'\n\n// Markdown-it\nimport MarkdownIt from 'markdown-it'\nimport mdAttrs from 'markdown-it-attrs'\nimport mdDecorate from 'markdown-it-decorate'\nimport { full as mdEmoji } from 'markdown-it-emoji'\nimport mdTaskLists from 'markdown-it-task-lists'\nimport mdExpandTabs from 'markdown-it-expand-tabs'\nimport mdAbbr from 'markdown-it-abbr'\nimport mdSup from 'markdown-it-sup'\nimport mdSub from 'markdown-it-sub'\nimport mdMark from 'markdown-it-mark'\nimport mdMultiTable from 'markdown-it-multimd-table'\nimport mdFootnote from 'markdown-it-footnote'\nimport mdImsize from 'markdown-it-imsize'\nimport katex from 'katex'\nimport underline from '../../libs/markdown-it-underline'\nimport 'katex/dist/contrib/mhchem'\nimport twemoji from 'twemoji'\nimport plantuml from './markdown/plantuml'\n\n// Prism (Syntax Highlighting)\nimport Prism from 'prismjs'\n\n// Mermaid\nimport mermaid from 'mermaid'\n\n// Helpers\nimport katexHelper from './common/katex'\nimport tabsetHelper from './markdown/tabset'\nimport cmFold from './common/cmFold'\n\n// ========================================\n// INIT\n// ========================================\n\n// Platform detection\nconst CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'\n\n// Prism Config\nPrism.plugins.autoloader.languages_path = '/_assets/js/prism/'\nPrism.plugins.NormalizeWhitespace.setDefaults({\n  'remove-trailing': true,\n  'remove-indent': true,\n  'left-trim': true,\n  'right-trim': true,\n  'remove-initial-line-feed': true,\n  'tabs-to-spaces': 2\n})\n\n// Markdown Instance\nconst md = new MarkdownIt({\n  html: true,\n  breaks: true,\n  linkify: true,\n  typography: true,\n  highlight(str, lang) {\n    if (lang === 'diagram') {\n      return `<pre class=\"diagram\">` + Buffer.from(str, 'base64').toString() + `</pre>`\n    } else if (['mermaid', 'plantuml'].includes(lang)) {\n      return `<pre class=\"codeblock-${lang}\"><code>${_.escape(str)}</code></pre>`\n    } else {\n      return `<pre class=\"line-numbers\"><code class=\"language-${lang}\">${_.escape(str)}</code></pre>`\n    }\n  }\n})\n  .use(mdAttrs, {\n    allowedAttributes: ['id', 'class', 'target']\n  })\n  .use(mdDecorate)\n  .use(underline)\n  .use(mdEmoji)\n  .use(mdTaskLists, { label: false, labelAfter: false })\n  .use(mdExpandTabs)\n  .use(mdAbbr)\n  .use(mdSup)\n  .use(mdSub)\n  .use(mdMultiTable, { multiline: true, rowspan: true, headerless: true })\n  .use(mdMark)\n  .use(mdFootnote)\n  .use(mdImsize)\n\n// DOMPurify fix for draw.io\nDOMPurify.addHook('uponSanitizeElement', (elm) => {\n  if (elm.querySelectorAll) {\n    const breaks = elm.querySelectorAll('foreignObject br, foreignObject p')\n    if (breaks && breaks.length) {\n      for (let i = 0; i < breaks.length; i++) {\n        breaks[i].parentNode.replaceChild(\n          document.createElement('div'),\n          breaks[i]\n        )\n      }\n    }\n  }\n})\n\n// ========================================\n// HELPER FUNCTIONS\n// ========================================\n\n// Inject line numbers for preview scroll sync\nlet linesMap = []\nfunction injectLineNumbers (tokens, idx, options, env, slf) {\n  let line\n  if (tokens[idx].map && tokens[idx].level === 0) {\n    line = tokens[idx].map[0]\n    tokens[idx].attrJoin('class', 'line')\n    tokens[idx].attrSet('data-line', String(line))\n    linesMap.push(line)\n  }\n  return slf.renderToken(tokens, idx, options, env, slf)\n}\nmd.renderer.rules.paragraph_open = injectLineNumbers\nmd.renderer.rules.heading_open = injectLineNumbers\nmd.renderer.rules.blockquote_open = injectLineNumbers\n\ncmFold.register('markdown')\n// ========================================\n// PLANTUML\n// ========================================\n\n// TODO: Use same options as defined in backend\nplantuml.init(md, {})\n\n// ========================================\n// KATEX\n// ========================================\n\nconst macros = {}\nmd.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline)\nmd.renderer.rules.katex_inline = (tokens, idx) => {\n  try {\n    return katex.renderToString(tokens[idx].content, {\n      displayMode: false, macros\n    })\n  } catch (err) {\n    console.warn(err)\n    return tokens[idx].content\n  }\n}\nmd.block.ruler.after('blockquote', 'katex_block', katexHelper.katexBlock, {\n  alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]\n})\nmd.renderer.rules.katex_block = (tokens, idx) => {\n  try {\n    return `<p>` + katex.renderToString(tokens[idx].content, {\n      displayMode: true, macros\n    }) + `</p>`\n  } catch (err) {\n    console.warn(err)\n    return tokens[idx].content\n  }\n}\n\n// ========================================\n// TWEMOJI\n// ========================================\n\nmd.renderer.rules.emoji = (token, idx) => {\n  return twemoji.parse(token[idx].content, {\n    callback (icon, opts) {\n      return `/_assets/svg/twemoji/${icon}.svg`\n    }\n  })\n}\n\n// ========================================\n// Vue Component\n// ========================================\n\nlet mermaidId = 0\n\nexport default {\n  components: {\n    markdownHelp\n  },\n  props: {\n    save: {\n      type: Function,\n      default: () => {}\n    }\n  },\n  data() {\n    return {\n      fabInsertMenu: false,\n      cm: null,\n      cursorPos: { ch: 0, line: 1 },\n      previewShown: true,\n      previewHTML: '',\n      helpShown: false,\n      spellModeActive: false,\n      insertLinkDialog: false\n    }\n  },\n  computed: {\n    isMobile() {\n      return this.$vuetify.breakpoint.smAndDown\n    },\n    isModalShown() {\n      return this.helpShown || this.activeModal !== ''\n    },\n    locale: get('page/locale'),\n    path: get('page/path'),\n    mode: get('editor/mode'),\n    activeModal: sync('editor/activeModal')\n  },\n  watch: {\n    previewShown (newValue, oldValue) {\n      if (newValue && !oldValue) {\n        this.$nextTick(() => {\n          this.renderMermaidDiagrams()\n          Prism.highlightAllUnder(this.$refs.editorPreview)\n          Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs'))\n        })\n      }\n    },\n    spellModeActive (newValue, oldValue) {\n      if (newValue) {\n        this.$nextTick(() => {\n          this.$refs.editorPreview.focus()\n        })\n      }\n    }\n  },\n  methods: {\n    toggleModal(key) {\n      this.activeModal = (this.activeModal === key) ? '' : key\n      this.helpShown = false\n    },\n    closeAllModal() {\n      this.activeModal = ''\n      this.helpShown = false\n    },\n    onCmInput: _.debounce(function (newContent) {\n      this.processContent(newContent)\n    }, 600),\n    onCmPaste (cm, ev) {\n      // const clipItems = (ev.clipboardData || ev.originalEvent.clipboardData).items\n      // for (let clipItem of clipItems) {\n      //   if (_.startsWith(clipItem.type, 'image/')) {\n      //     const file = clipItem.getAsFile()\n      //     const reader = new FileReader()\n      //     reader.onload = evt => {\n      //       this.$store.commit(`loadingStart`, 'editor-paste-image')\n      //       this.insertAfter({\n      //         content: `![${file.name}](${evt.target.result})`,\n      //         newLine: true\n      //       })\n      //     }\n      //     reader.readAsDataURL(file)\n      //   }\n      // }\n    },\n    processContent (newContent) {\n      linesMap = []\n      // this.$store.set('editor/content', newContent)\n      this.processMarkers(this.cm.firstLine(), this.cm.lastLine())\n      this.previewHTML = DOMPurify.sanitize(md.render(newContent), {\n        ADD_TAGS: ['foreignObject'],\n        HTML_INTEGRATION_POINTS: { foreignobject: true }\n      })\n      this.$nextTick(() => {\n        tabsetHelper.format()\n        this.renderMermaidDiagrams()\n        Prism.highlightAllUnder(this.$refs.editorPreview)\n        Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs'))\n        this.scrollSync(this.cm)\n      })\n    },\n    /**\n     * Update cursor state\n     */\n    positionSync(cm) {\n      this.cursorPos = cm.getCursor('head')\n    },\n    /**\n     * Wrap selection with start / end tags\n     */\n    toggleMarkup({ start, end }) {\n      if (!end) { end = start }\n      if (!this.cm.doc.somethingSelected()) {\n        return this.$store.commit('showNotification', {\n          message: this.$t('editor:markup.noSelectionError'),\n          style: 'warning',\n          icon: 'warning'\n        })\n      }\n      this.cm.doc.replaceSelections(this.cm.doc.getSelections().map(s => start + s + end))\n    },\n    /**\n     * Set current line as header\n     */\n    setHeaderLine(lvl) {\n      const curLine = this.cm.doc.getCursor('head').line\n      let lineContent = this.cm.doc.getLine(curLine)\n      const lineLength = lineContent.length\n      if (_.startsWith(lineContent, '#')) {\n        lineContent = lineContent.replace(/^(#+ )/, '')\n      }\n      lineContent = _.times(lvl, n => '#').join('') + ` ` + lineContent\n      this.cm.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength })\n    },\n    /**\n     * Get the header lever of the current line\n     */\n    getHeaderLevel(cm) {\n      const curLine = this.cm.doc.getCursor('head').line\n      let lineContent = this.cm.doc.getLine(curLine)\n      let lvl = 0\n\n      const result = lineContent.match(/^(#+) /)\n      if (result) {\n        lvl = _.get(result, '[1]', '').length\n      }\n      return lvl\n    },\n    /**\n     * Insert content at cursor\n     */\n    insertAtCursor({ content }) {\n      const cursor = this.cm.doc.getCursor('head')\n      this.cm.doc.replaceRange(content, cursor)\n    },\n    /**\n     * Insert content after current line\n     */\n    insertAfter({ content, newLine }) {\n      const curLine = this.cm.doc.getCursor('to').line\n      const lineLength = this.cm.doc.getLine(curLine).length\n      this.cm.doc.replaceRange(newLine ? `\\n${content}\\n` : content, { line: curLine, ch: lineLength + 1 })\n    },\n    /**\n     * Insert content before current line\n     */\n    insertBeforeEachLine({ content, after }) {\n      let lines = []\n      if (!this.cm.doc.somethingSelected()) {\n        lines.push(this.cm.doc.getCursor('head').line)\n      } else {\n        lines = _.flatten(this.cm.doc.listSelections().map(sl => {\n          const range = Math.abs(sl.anchor.line - sl.head.line) + 1\n          const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line\n          return _.times(range, l => l + lowestLine)\n        }))\n      }\n      lines.forEach(ln => {\n        let lineContent = this.cm.doc.getLine(ln)\n        const lineLength = lineContent.length\n        if (_.startsWith(lineContent, content)) {\n          lineContent = lineContent.substring(content.length)\n        }\n\n        this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })\n      })\n      if (after) {\n        const lastLine = _.last(lines)\n        this.cm.doc.replaceRange(`\\n${after}\\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })\n      }\n    },\n    /**\n     * Update scroll sync\n     */\n    scrollSync: _.debounce(function (cm) {\n      if (!this.previewShown || cm.somethingSelected()) { return }\n      let currentLine = cm.getCursor().line\n      if (currentLine < 3) {\n        this.Velocity(this.$refs.editorPreview, 'stop', true)\n        this.Velocity(this.$refs.editorPreview.firstChild, 'scroll', { offset: '-50', duration: 1000, container: this.$refs.editorPreviewContainer })\n      } else {\n        let closestLine = _.findLast(linesMap, n => n <= currentLine)\n        let destElm = this.$refs.editorPreview.querySelector(`[data-line='${closestLine}']`)\n        if (destElm) {\n          this.Velocity(this.$refs.editorPreview, 'stop', true)\n          this.Velocity(destElm, 'scroll', { offset: '-100', duration: 1000, container: this.$refs.editorPreviewContainer })\n        }\n      }\n    }, 500),\n    toggleHelp () {\n      this.helpShown = !this.helpShown\n      this.activeModal = ''\n    },\n    toggleFullscreen () {\n      this.cm.setOption('fullScreen', true)\n    },\n    refresh() {\n      this.$nextTick(() => {\n        this.cm.refresh()\n      })\n    },\n    renderMermaidDiagrams () {\n      document.querySelectorAll('.editor-markdown-preview pre.codeblock-mermaid > code').forEach(elm => {\n        mermaidId++\n        const mermaidDef = elm.innerText\n        const mmElm = document.createElement('div')\n        mmElm.innerHTML = `<div id=\"mermaid-id-${mermaidId}\">${mermaid.render(`mermaid-id-${mermaidId}`, mermaidDef)}</div>`\n        elm.parentElement.replaceWith(mmElm)\n      })\n    },\n    autocomplete (cm, change) {\n      if (cm.getModeAt(cm.getCursor()).name !== 'markdown') {\n        return\n      }\n\n      // Links\n      if (change.text[0] === '(') {\n        const curLine = cm.getLine(change.from.line).substring(0, change.from.ch)\n        if (curLine[curLine.length - 1] === ']') {\n          cm.showHint({\n            hint: async (cm, options) => {\n              const cur = cm.getCursor()\n              const curLine = cm.getLine(cur.line).substring(0, cur.ch)\n              const queryString = curLine.substring(curLine.lastIndexOf('[') + 1, curLine.length - 2)\n              const token = cm.getTokenAt(cur)\n              try {\n                const respRaw = await this.$apollo.query({\n                  query: gql`\n                    query ($query: String!, $locale: String) {\n                      pages {\n                        search(query:$query, locale:$locale) {\n                          results {\n                            title\n                            path\n                            locale\n                          }\n                          totalHits\n                        }\n                      }\n                    }\n                  `,\n                  variables: {\n                    query: queryString,\n                    locale: this.locale\n                  },\n                  fetchPolicy: 'cache-first'\n                })\n                const resp = _.get(respRaw, 'data.pages.search', {})\n                if (resp && resp.totalHits > 0) {\n                  return {\n                    list: resp.results.map(r => ({\n                      text: '(' + (siteLangs.length > 0 ? `/${r.locale}/${r.path}` : `/${r.path}`) + ')',\n                      displayText: siteLangs.length > 0 ? `/${r.locale}/${r.path} - ${r.title}` : `/${r.path} - ${r.title}`\n                    })),\n                    from: CodeMirror.Pos(cur.line, token.start),\n                    to: CodeMirror.Pos(cur.line, token.end)\n                  }\n                }\n              } catch (err) {}\n              return {\n                list: [],\n                from: CodeMirror.Pos(cur.line, token.start),\n                to: CodeMirror.Pos(cur.line, token.end)\n              }\n            }\n          })\n        }\n      }\n    },\n    insertLink () {\n      this.insertLinkDialog = true\n    },\n    insertLinkHandler ({ locale, path }) {\n      const lastPart = _.last(path.split('/'))\n      this.insertAtCursor({\n        content: siteLangs.length > 0 ? `[${lastPart}](/${locale}/${path})` : `[${lastPart}](/${path})`\n      })\n    },\n    processMarkers (from, to) {\n      let found = null\n      let foundStart = 0\n      this.cm.doc.getAllMarks().forEach(mk => {\n        if (mk.__kind) {\n          mk.clear()\n        }\n      })\n      this.cm.eachLine(from, to, ln => {\n        const line = ln.lineNo()\n        if (ln.text.startsWith('```diagram')) {\n          found = 'diagram'\n          foundStart = line\n        } else if (ln.text === '```' && found) {\n          switch (found) {\n            // ------------------------------\n            // -> DIAGRAM\n            // ------------------------------\n            case 'diagram': {\n              if (line - foundStart !== 2) {\n                return\n              }\n              this.addMarker({\n                kind: 'diagram',\n                from: { line: foundStart, ch: 3 },\n                to: { line: foundStart, ch: 10 },\n                text: 'Edit Diagram',\n                action: ((start, end) => {\n                  return (ev) => {\n                    this.cm.doc.setSelection({ line: start, ch: 0 }, { line: end, ch: 3 })\n                    try {\n                      const raw = this.cm.doc.getLine(end - 1)\n                      this.$store.set('editor/activeModalData', Buffer.from(raw, 'base64').toString())\n                      this.toggleModal(`editorModalDrawio`)\n                    } catch (err) {\n                      return this.$store.commit('showNotification', {\n                        message: 'Failed to process diagram data.',\n                        style: 'warning',\n                        icon: 'warning'\n                      })\n                    }\n                  }\n                })(foundStart, line)\n              })\n              if (ln.height > 0) {\n                this.cm.foldCode(foundStart)\n              }\n              break\n            }\n          }\n          found = null\n        }\n      })\n    },\n    addMarker ({ kind, from, to, text, action }) {\n      const markerElm = document.createElement('span')\n      markerElm.appendChild(document.createTextNode(text))\n      markerElm.className = 'CodeMirror-buttonmarker'\n      markerElm.addEventListener('click', action)\n      this.cm.markText(from, to, { replacedWith: markerElm, __kind: kind })\n    }\n  },\n  mounted() {\n    this.$store.set('editor/editorKey', 'markdown')\n\n    if (this.mode === 'create' && !this.$store.get('editor/content')) {\n      this.$store.set('editor/content', '# Header\\nYour content here')\n    }\n\n    // Initialize Mermaid API\n    mermaid.initialize({\n      startOnLoad: false,\n      theme: this.$vuetify.theme.dark ? `dark` : `default`\n    })\n\n    // Initialize CodeMirror\n\n    this.cm = CodeMirror.fromTextArea(this.$refs.cm, {\n      tabSize: 2,\n      mode: 'text/markdown',\n      theme: 'wikijs-dark',\n      lineNumbers: true,\n      lineWrapping: true,\n      line: true,\n      styleActiveLine: true,\n      highlightSelectionMatches: {\n        annotateScrollbar: true\n      },\n      viewportMargin: 50,\n      inputStyle: 'contenteditable',\n      allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],\n      direction: siteConfig.rtl ? 'rtl' : 'ltr',\n      foldGutter: true,\n      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']\n    })\n    this.cm.setValue(this.$store.get('editor/content'))\n    this.cm.on('change', c => {\n      this.$store.set('editor/content', c.getValue())\n      this.onCmInput(this.$store.get('editor/content'))\n    })\n    if (this.$vuetify.breakpoint.mdAndUp) {\n      this.cm.setSize(null, 'calc(100vh - 112px - 24px)')\n    } else {\n      this.cm.setSize(null, 'calc(100vh - 112px - 16px)')\n    }\n\n    // Set Keybindings\n\n    const keyBindings = {\n      'F11' (c) {\n        c.setOption('fullScreen', !c.getOption('fullScreen'))\n      },\n      'Esc' (c) {\n        if (c.getOption('fullScreen')) c.setOption('fullScreen', false)\n      }\n    }\n    _.set(keyBindings, `${CtrlKey}-S`, c => {\n      this.save()\n      return false\n    })\n    _.set(keyBindings, `${CtrlKey}-B`, c => {\n      this.toggleMarkup({ start: `**` })\n      return false\n    })\n    _.set(keyBindings, `${CtrlKey}-I`, c => {\n      this.toggleMarkup({ start: `*` })\n      return false\n    })\n    _.set(keyBindings, `${CtrlKey}-Alt-Right`, c => {\n      let lvl = this.getHeaderLevel(c)\n      if (lvl >= 6) { lvl = 5 }\n      this.setHeaderLine(lvl + 1)\n      return false\n    })\n    _.set(keyBindings, `${CtrlKey}-Alt-Left`, c => {\n      let lvl = this.getHeaderLevel(c)\n      if (lvl <= 1) { lvl = 2 }\n      this.setHeaderLine(lvl - 1)\n      return false\n    })\n    this.cm.setOption('extraKeys', keyBindings)\n\n    this.cm.on('inputRead', this.autocomplete)\n\n    // Handle cursor movement\n\n    this.cm.on('cursorActivity', c => {\n      this.positionSync(c)\n      this.scrollSync(c)\n    })\n\n    // Handle special paste\n\n    this.cm.on('paste', this.onCmPaste)\n\n    // Render initial preview\n\n    this.processContent(this.$store.get('editor/content'))\n    this.refresh()\n\n    this.$root.$on('editorInsert', opts => {\n      switch (opts.kind) {\n        case 'IMAGE':\n          let img = `![${opts.text}](${opts.path})`\n          if (opts.align && opts.align !== '') {\n            img += `{.align-${opts.align}}`\n          }\n          this.insertAtCursor({\n            content: img\n          })\n          break\n        case 'BINARY':\n          this.insertAtCursor({\n            content: `[${opts.text}](${opts.path})`\n          })\n          break\n        case 'DIAGRAM':\n          const selStartLine = this.cm.getCursor('from').line\n          const selEndLine = this.cm.getCursor('to').line + 1\n          this.cm.doc.replaceSelection('```diagram\\n' + opts.text + '\\n```\\n', 'start')\n          this.processMarkers(selStartLine, selEndLine)\n          break\n      }\n    })\n\n    // Handle save conflict\n    this.$root.$on('saveConflict', () => {\n      this.toggleModal(`editorModalConflict`)\n    })\n    this.$root.$on('overwriteEditorContent', () => {\n      this.cm.setValue(this.$store.get('editor/content'))\n    })\n  },\n  beforeDestroy() {\n    this.$root.$off('editorInsert')\n  }\n}\n</script>\n\n<style lang='scss'>\n\n$editor-height: calc(100vh - 112px - 24px);\n$editor-height-mobile: calc(100vh - 112px - 16px);\n\n.editor-markdown {\n  &-main {\n    display: flex;\n    width: 100%;\n  }\n\n  &-editor {\n    background-color: darken(mc('grey', '900'), 4.5%);\n    flex: 1 1 50%;\n    display: block;\n    height: $editor-height;\n    position: relative;\n\n    @include until($tablet) {\n      height: $editor-height-mobile;\n    }\n  }\n\n  &-preview {\n    flex: 1 1 50%;\n    background-color: mc('grey', '100');\n    position: relative;\n    height: $editor-height;\n    overflow: hidden;\n    padding: 1rem;\n\n    @at-root .theme--dark & {\n      background-color: mc('grey', '900');\n    }\n\n    @include until($tablet) {\n      display: none;\n    }\n\n    &-enter-active, &-leave-active {\n      transition: max-width .5s ease;\n      max-width: 50vw;\n\n      .editor-code-preview-content {\n        width: 50vw;\n        overflow:hidden;\n      }\n    }\n    &-enter, &-leave-to {\n      max-width: 0;\n    }\n\n    &-content {\n      height: $editor-height;\n      overflow-y: scroll;\n      padding: 0;\n      width: calc(100% + 17px);\n      // -ms-overflow-style: none;\n\n      // &::-webkit-scrollbar {\n      //   width: 0px;\n      //   background: transparent;\n      // }\n\n      @include until($tablet) {\n        height: $editor-height-mobile;\n      }\n\n      > div {\n        outline: none;\n      }\n\n      p.line {\n        overflow-wrap: break-word;\n      }\n\n      .tabset {\n        background-color: mc('teal', '700');\n        color: mc('teal', '100') !important;\n        padding: 5px 12px;\n        font-size: 14px;\n        font-weight: 500;\n        border-radius: 5px 0 0 0;\n        font-style: italic;\n\n        &::after {\n          display: none;\n        }\n\n        &-header {\n          background-color: mc('teal', '500');\n          color: #FFF !important;\n          padding: 5px 12px;\n          font-size: 14px;\n          font-weight: 500;\n          margin-top: 0 !important;\n\n          &::after {\n            display: none;\n          }\n        }\n\n        &-content {\n          border-left: 5px solid mc('teal', '500');\n          background-color: mc('teal', '50');\n          padding: 0 15px 15px;\n          overflow: hidden;\n\n          @at-root .theme--dark & {\n            background-color: rgba(mc('teal', '500'), .1);\n          }\n        }\n      }\n    }\n  }\n\n  &-toolbar {\n    background-color: mc('blue', '700');\n    background-image: linear-gradient(to bottom, mc('blue', '700') 0%, mc('blue','800') 100%);\n    color: #FFF;\n\n    .v-toolbar__content {\n      padding-left: 64px;\n\n      @include until($tablet) {\n        padding-left: 8px;\n      }\n    }\n  }\n\n  &-insert:not(.v-speed-dial--right) {\n    @include from($tablet) {\n      left: 50%;\n      margin-left: -28px;\n    }\n  }\n\n  &-sidebar {\n    background-color: mc('grey', '900');\n    width: 64px;\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-start;\n    align-items: center;\n    padding: 24px 0;\n\n    @include until($tablet) {\n      padding: 12px 0;\n      width: 40px;\n    }\n  }\n\n  &-sysbar {\n    padding-left: 0;\n\n    &-locale {\n      background-color: rgba(255,255,255,.25);\n      display:inline-flex;\n      padding: 0 12px;\n      height: 24px;\n      width: 63px;\n      justify-content: center;\n      align-items: center;\n    }\n  }\n\n  // ==========================================\n  // Fix FAB revealing under codemirror\n  // ==========================================\n\n  .speed-dial--fixed {\n    z-index: 8;\n  }\n\n  // ==========================================\n  // CODE MIRROR\n  // ==========================================\n\n  .CodeMirror {\n    height: auto;\n    font-family: 'Roboto Mono', monospace;\n    font-size: .9rem;\n\n    .cm-header-1 {\n      font-size: 1.5rem;\n    }\n    .cm-header-2 {\n      font-size: 1.25rem;\n    }\n    .cm-header-3 {\n      font-size: 1.15rem;\n    }\n    .cm-header-4 {\n      font-size: 1.1rem;\n    }\n    .cm-header-5 {\n      font-size: 1.05rem;\n    }\n    .cm-header-6 {\n      font-size: 1.025rem;\n    }\n  }\n\n  .CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like {\n    word-break: break-word;\n  }\n\n  .CodeMirror-focused .cm-matchhighlight {\n    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);\n    background-position: bottom;\n    background-repeat: repeat-x;\n  }\n  .cm-matchhighlight {\n    background-color: mc('grey', '800');\n  }\n  .CodeMirror-selection-highlight-scrollbar {\n    background-color: mc('green', '600');\n  }\n}\n\n// HINT DROPDOWN\n\n.CodeMirror-hints {\n  position: absolute;\n  z-index: 10;\n  overflow: hidden;\n  list-style: none;\n\n  margin: 0;\n  padding: 1px;\n\n  box-shadow: 2px 3px 5px rgba(0,0,0,.2);\n  border: 1px solid mc('grey', '700');\n\n  background: mc('grey', '900');\n  font-family: 'Roboto Mono', monospace;\n  font-size: .9rem;\n\n  max-height: 150px;\n  overflow-y: auto;\n\n  min-width: 250px;\n  max-width: 80vw;\n}\n\n.CodeMirror-hint {\n  margin: 0;\n  padding: 0 4px;\n  white-space: pre;\n  color: #FFF;\n  cursor: pointer;\n}\n\nli.CodeMirror-hint-active {\n  background: mc('blue', '500');\n  color: #FFF;\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-blocks.vue",
    "content": "<template lang='pug'>\n  v-card.editor-modal-blocks.animated.fadeInLeft(flat, tile)\n    v-container.pa-3(grid-list-lg, fluid)\n      v-row(dense)\n        v-col(\n          v-for='(item, idx) of blocks'\n          :key='`block-` + item.key'\n          cols='12'\n          lg='4'\n          xl='3'\n          )\n          v-card.radius-7(light, flat, @click='selectBlock(item)')\n            v-card-text\n              .d-flex.align-center\n                v-avatar.radius-7(color='teal')\n                  v-icon(dark) {{item.icon}}\n                .pl-3\n                  .body-2: strong.teal--text {{item.title}}\n                  .caption.grey--text {{item.description}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync } from 'vuex-pathify'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      blocks: [\n        {\n          key: 'childlist',\n          title: 'List Children Pages',\n          description: 'Display a links list of all children of this page.',\n          icon: 'mdi-format-list-text'\n        },\n        {\n          key: 'tabs',\n          title: 'Tabs',\n          description: 'Organize content within tabs.',\n          icon: 'mdi-tab'\n        }\n      ]\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n    selectBlock (item) {\n      this.block = _.cloneDeep(item)\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.editor-modal-blocks {\n    position: fixed;\n    top: 112px;\n    left: 64px;\n    z-index: 10;\n    width: calc(100vw - 64px - 17px);\n    height: calc(100vh - 112px - 24px);\n    background-color: rgba(darken(mc('grey', '900'), 3%), .9) !important;\n\n    @include until($tablet) {\n      left: 40px;\n      width: calc(100vw - 40px);\n    }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-conflict.vue",
    "content": "<template lang='pug'>\n  v-card.editor-modal-conflict.animated.fadeIn(flat, tile)\n    .pa-4\n      v-toolbar.radius-7(flat, color='indigo', style='border-bottom-left-radius: 0; border-bottom-right-radius: 0;', dark)\n        v-icon.mr-3 mdi-merge\n        .subtitle-1 {{$t('editor:conflict.title')}}\n        v-spacer\n        v-btn(outlined, color='white', @click='useLocal', :title='$t(`editor:conflict.useLocalHint`)')\n          v-icon(left) mdi-alpha-l-box\n          span {{$t('editor:conflict.useLocal')}}\n        v-dialog(\n          v-model='isRemoteConfirmDiagShown'\n          width='500'\n          )\n          template(v-slot:activator='{ on }')\n            v-btn.ml-3(outlined, color='white', v-on='on', :title='$t(`editor:conflict.useRemoteHint`)')\n              v-icon(left) mdi-alpha-r-box\n              span {{$t('editor:conflict.useRemote')}}\n          v-card\n            .dialog-header.is-short.is-indigo\n              v-icon.mr-3(color='white') mdi-alpha-r-box\n              span {{$t('editor:conflict.overwrite.title')}}\n            v-card-text.pa-4\n              i18next.body-2(tag='div', path='editor:conflict.overwrite.description')\n                strong(place='refEditsLost') {{$t('editor:conflict.overwrite.editsLost')}}\n            v-card-chin\n              v-spacer\n              v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')\n                v-icon(left) mdi-close\n                span {{$t('common:actions.cancel')}}\n              v-btn(@click='useRemote', color='indigo', dark)\n                v-icon(left) mdi-check\n                span {{$t('common:actions.confirm')}}\n        v-divider.mx-3(vertical)\n        v-btn(outlined, color='indigo lighten-4', @click='close')\n          v-icon(left) mdi-close\n          span {{$t('common:actions.cancel')}}\n      v-row.indigo.darken-1.body-2(no-gutters)\n        v-col.pa-4\n          v-icon.mr-3(color='white') mdi-alpha-l-box\n          i18next.white--text(tag='span', path='editor:conflict.localVersion')\n            em.indigo--text.text--lighten-4(place='refEditable') {{$t('editor:conflict.editable')}}\n        v-divider(vertical)\n        v-col.pa-4\n          v-icon.mr-3(color='white') mdi-alpha-r-box\n          i18next.white--text(tag='span', path='editor:conflict.remoteVersion')\n            em.indigo--text.text--lighten-4(place='refReadOnly') {{$t('editor:conflict.readonly')}}\n      v-row.grey.lighten-2.body-2(no-gutters)\n        v-col.px-4.py-2\n          i18next.grey--text.text--darken-2(tag='em', path='editor:conflict.leftPanelInfo')\n            span(place='date', :title='$options.filters.moment(checkoutDateActive, `LLL`)') {{ checkoutDateActive | moment('from') }}\n        v-divider(vertical)\n        v-col.px-4.py-2\n          i18next.grey--text.text--darken-2(tag='em', path='editor:conflict.rightPanelInfo')\n            strong(place='authorName') {{latest.authorName}}\n            span(place='date', :title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}\n      v-row.grey.lighten-3.grey--text.text--darken-3(no-gutters)\n        v-col.pa-4\n          .body-2\n            strong.indigo--text {{$t('editor:conflict.pageTitle')}}\n            strong.pl-2 {{title}}\n          .caption\n            strong.indigo--text {{$t('editor:conflict.pageDescription')}}\n            span.pl-2 {{description}}\n        v-divider(vertical, light)\n        v-col.pa-4\n          .body-2\n            strong.indigo--text {{$t('editor:conflict.pageTitle')}}\n            strong.pl-2 {{latest.title}}\n          .caption\n            strong.indigo--text {{$t('editor:conflict.pageDescription')}}\n            span.pl-2 {{latest.description}}\n      v-card.radius-7(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')\n        div(ref='cm')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { sync, get } from 'vuex-pathify'\n\n/* global siteConfig */\n\n// ========================================\n// IMPORTS\n// ========================================\n\nimport '../../libs/codemirror-merge/diff-match-patch.js'\n\n// Code Mirror\nimport CodeMirror from 'codemirror'\nimport 'codemirror/lib/codemirror.css'\n\n// Language\nimport 'codemirror/mode/markdown/markdown.js'\nimport 'codemirror/mode/htmlmixed/htmlmixed.js'\n\n// Addons\nimport 'codemirror/addon/selection/active-line.js'\nimport 'codemirror/addon/merge/merge.js'\nimport 'codemirror/addon/merge/merge.css'\n\nexport default {\n  data() {\n    return {\n      cm: null,\n      latest: {\n        title: '',\n        description: '',\n        updatedAt: '',\n        authorName: ''\n      },\n      isRemoteConfirmDiagShown: false\n    }\n  },\n  computed: {\n    editorKey: get('editor/editorKey'),\n    activeModal: sync('editor/activeModal'),\n    pageId: get('page/id'),\n    title: get('page/title'),\n    description: get('page/description'),\n    updatedAt: get('page/updatedAt'),\n    checkoutDateActive: sync('editor/checkoutDateActive')\n  },\n  methods: {\n    close () {\n      this.isRemoteConfirmDiagShown = false\n      this.activeModal = ''\n    },\n    overwriteAndClose() {\n      this.checkoutDateActive = this.latest.updatedAt\n      this.$root.$emit('overwriteEditorContent')\n      this.$root.$emit('resetEditorConflict')\n      this.close()\n    },\n    useLocal () {\n      this.$store.set('editor/content', this.cm.edit.getValue())\n      this.overwriteAndClose()\n    },\n    useRemote () {\n      this.$store.set('editor/content', this.latest.content)\n      this.overwriteAndClose()\n    }\n  },\n  async mounted () {\n    let textMode = 'text/html'\n\n    switch (this.editorKey) {\n      case 'markdown':\n        textMode = 'text/markdown'\n        break\n    }\n\n    let resp = await this.$apollo.query({\n      query: gql`\n        query ($id: Int!) {\n          pages {\n            conflictLatest(id: $id) {\n              id\n              authorId\n              authorName\n              content\n              createdAt\n              description\n              isPublished\n              locale\n              path\n              tags\n              title\n              updatedAt\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      variables: {\n        id: this.$store.get('page/id')\n      }\n    })\n    resp = _.get(resp, 'data.pages.conflictLatest', false)\n\n    if (!resp) {\n      return this.$store.commit('showNotification', {\n        message: 'Failed to fetch latest version.',\n        style: 'warning',\n        icon: 'warning'\n      })\n    }\n    this.latest = resp\n\n    this.cm = CodeMirror.MergeView(this.$refs.cm, {\n      value: this.$store.get('editor/content'),\n      orig: resp.content,\n      tabSize: 2,\n      mode: textMode,\n      lineNumbers: true,\n      lineWrapping: true,\n      connect: null,\n      highlightDifferences: true,\n      styleActiveLine: true,\n      collapseIdentical: true,\n      direction: siteConfig.rtl ? 'rtl' : 'ltr'\n    })\n    this.cm.rightOriginal().setSize(null, 'calc(100vh - 265px)')\n    this.cm.editor().setSize(null, 'calc(100vh - 265px)')\n    this.cm.wrap.style.height = 'calc(100vh - 265px)'\n  }\n}\n</script>\n\n<style lang='scss'>\n.editor-modal-conflict {\n  position: fixed !important;\n  top: 0;\n  left: 0;\n  z-index: 10;\n  width: 100%;\n  height: 100vh;\n  background-color: rgba(0, 0, 0, .9) !important;\n  overflow: auto;\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-drawio.vue",
    "content": "<template lang='pug'>\n  v-card.editor-modal-drawio.animated.fadeIn(flat, tile)\n    iframe(\n      ref='drawio'\n      src='https://embed.diagrams.net/?embed=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0'\n      frameborder='0'\n    )\n</template>\n\n<script>\nimport { sync, get } from 'vuex-pathify'\n\n// const xmlTest = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n// <mxfile version=\"13.4.2\">\n//   <diagram id=\"SgbkCjxR32CZT1FvBvkp\" name=\"Page-1\">\n//     <mxGraphModel dx=\"2062\" dy=\"1123\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"850\" pageHeight=\"1100\" math=\"0\" shadow=\"0\">\n//       <root>\n//         <mxCell id=\"0\" />\n//         <mxCell id=\"1\" parent=\"0\" />\n//         <mxCell id=\"5gE3BTvRYS_8FoJnOusC-1\" value=\"\" style=\"whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;\" vertex=\"1\" parent=\"1\">\n//           <mxGeometry x=\"380\" y=\"530\" width=\"80\" height=\"80\" as=\"geometry\" />\n//         </mxCell>\n//       </root>\n//     </mxGraphModel>\n//   </diagram>\n// </mxfile>\n// `\n\nexport default {\n  data() {\n    return {\n      content: ''\n    }\n  },\n  computed: {\n    editorKey: get('editor/editorKey'),\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n    close () {\n      this.activeModal = ''\n    },\n    overwriteAndClose() {\n      this.$root.$emit('overwriteEditorContent')\n      this.$root.$emit('resetEditorConflict')\n      this.close()\n    },\n    send (msg) {\n      this.$refs.drawio.contentWindow.postMessage(JSON.stringify(msg), '*')\n    },\n    receive (evt) {\n      if (evt.frame === null || evt.source !== this.$refs.drawio.contentWindow || evt.data.length < 1) {\n        return\n      }\n      try {\n        const msg = JSON.parse(evt.data)\n        switch (msg.event) {\n          case 'init': {\n            this.send({\n              action: 'load',\n              autosave: 0,\n              modified: 'unsavedChanges',\n              xml: this.$store.get('editor/activeModalData'),\n              title: this.$store.get('page/title')\n            })\n            this.$store.set('editor/activeModalData', null)\n            break\n          }\n          case 'save': {\n            if (msg.exit) {\n              this.send({\n                action: 'export',\n                format: 'xmlsvg'\n              })\n            }\n            break\n          }\n          case 'export': {\n            const svgDataStart = msg.data.indexOf('base64,') + 7\n            this.$root.$emit('editorInsert', {\n              kind: 'DIAGRAM',\n              text: msg.data.slice(svgDataStart)\n              // text: msg.xml.replace(/ agent=\"(.*?)\"/, '').replace(/ host=\"(.*?)\"/, '').replace(/ etag=\"(.*?)\"/, '')\n            })\n            this.close()\n            break\n          }\n          case 'exit': {\n            this.close()\n            break\n          }\n        }\n      } catch (err) {\n        console.error(err)\n      }\n    }\n  },\n  async mounted () {\n    window.addEventListener('message', this.receive)\n  },\n  beforeDestroy () {\n    window.removeEventListener('message', this.receive)\n  }\n}\n</script>\n\n<style lang='scss'>\n.editor-modal-drawio {\n  position: fixed !important;\n  top: 0;\n  left: 0;\n  z-index: 10;\n  width: 100%;\n  height: 100vh;\n  background-color: rgba(255,255,255, 1) !important;\n  overflow: hidden;\n\n  > iframe {\n    width: 100%;\n    height: 100vh;\n    border: 0;\n    padding: 0;\n    background-color: #FFF;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-editorselect.vue",
    "content": "<template lang='pug'>\n  v-dialog(v-model='isShown', persistent, max-width='700', no-click-animation)\n    v-btn(fab, fixed, bottom, right, color='grey darken-3', dark, @click='goBack', style='width: 50px;'): v-icon mdi-undo-variant\n    v-card.radius-7(color='blue darken-3', dark)\n      v-card-text.text-center.py-4\n        .subtitle-1.white--text {{$t('editor:select.title')}}\n        v-container(grid-list-lg, fluid)\n          v-layout(row, wrap, justify-center)\n            v-flex(xs6)\n              v-card.radius-7.animated.fadeInUp.wait-p1s(\n                hover\n                light\n                ripple\n                )\n                v-card-text.text-center(@click='selectEditor(\"markdown\")')\n                  img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')\n                  .body-2.primary--text.mt-2 Markdown\n                  .caption.grey--text Plain Text Formatting\n            v-flex(xs6)\n              v-card.radius-7.animated.fadeInUp.wait-p2s(\n                hover\n                light\n                ripple\n                )\n                v-card-text.text-center(@click='selectEditor(\"ckeditor\")')\n                  img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')\n                  .body-2.mt-2.primary--text Visual Editor\n                  .caption.grey--text Rich-text WYSIWYG\n            v-flex(xs4)\n              v-card.radius-7.animated.fadeInUp.wait-p3s(\n                hover\n                light\n                ripple\n                )\n                v-card-text.text-center(@click='selectEditor(\"asciidoc\")')\n                  img(src='/_assets/svg/editor-icon-asciidoc.svg', alt='AsciiDoc', style='width: 36px;')\n                  .body-2.primary--text.mt-2 AsciiDoc\n                  .caption.grey--text Plain Text Formatting\n            v-flex(xs4)\n              v-card.radius-7.animated.fadeInUp.wait-p4s(\n                hover\n                light\n                ripple\n                )\n                v-card-text.text-center(@click='selectEditor(\"code\")')\n                  img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')\n                  .body-2.primary--text.mt-2 Code\n                  .caption.grey--text Raw HTML\n            v-flex(xs4)\n              v-card.radius-7.animated.fadeInUp.wait-p5s(\n                hover\n                light\n                ripple\n                )\n                v-card-text.text-center(@click='fromTemplate')\n                  img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')\n                  .body-2.mt-1.teal--text From Template\n                  .caption.grey--text Use an existing page...\n\n    page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync, get } from 'vuex-pathify'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      templateDialogIsShown: false\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    currentEditor: sync('editor/editor'),\n    locale: get('page/locale'),\n    path: get('page/path')\n  },\n  methods: {\n    selectEditor (name) {\n      this.currentEditor = `editor${_.startCase(name)}`\n      this.isShown = false\n    },\n    goBack () {\n      window.history.go(-1)\n    },\n    fromTemplate () {\n      this.templateDialogIsShown = true\n    },\n    fromTemplateHandle ({ id }) {\n      this.templateDialogIsShown = false\n      this.isShown = false\n      this.$nextTick(() => {\n        window.location.assign(`/e/${this.locale}/${this.path}?from=${id}`)\n      })\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-media.vue",
    "content": "<template lang='pug'>\n  v-card.editor-modal-media.animated.fadeInLeft(flat, tile, :class='`is-editor-` + editorKey')\n    v-container.pa-3(grid-list-lg, fluid)\n      v-layout(row, wrap)\n        v-flex(xs12, lg9)\n          v-card.radius-7.animated.fadeInLeft.wait-p1s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')\n            v-card-text\n              .d-flex\n                v-toolbar.radius-7(:color='$vuetify.theme.dark ? `teal` : `teal lighten-5`', dense, flat, height='44')\n                  .body-2(:class='$vuetify.theme.dark ? `white--text` : `teal--text`') {{$t('editor:assets.title')}}\n                  v-spacer\n                  v-btn(text, icon, @click='refresh')\n                    v-icon(:color='$vuetify.theme.dark ? `white` : `teal`') mdi-refresh\n                v-dialog(v-model='newFolderDialog', max-width='550')\n                  template(v-slot:activator='{ on }')\n                    v-btn.ml-3.my-0.mr-0.radius-7(outlined, large, color='teal', :icon='$vuetify.breakpoint.xsOnly', v-on='on')\n                      v-icon(:left='$vuetify.breakpoint.mdAndUp') mdi-plus\n                      span.hidden-sm-and-down(:class='$vuetify.theme.dark ? `teal--text text--lighten-3` : ``') {{$t('editor:assets.newFolder')}}\n                  v-card\n                    .dialog-header.is-short.subtitle-1 {{$t('editor:assets.newFolder')}}\n                    v-card-text.pt-5\n                      v-text-field.md2(\n                        outlined\n                        prepend-icon='mdi-folder-outline'\n                        v-model='newFolderName'\n                        :label='$t(`editor:assets.folderName`)'\n                        counter='255'\n                        @keyup.enter='createFolder'\n                        @keyup.esc='newFolderDialog = false'\n                        ref='folderNameIpt'\n                        )\n                      i18next.caption.grey--text.text--darken-1.pl-5(path='editor:assets.folderNameNamingRules', tag='div')\n                        a(place='namingRules', href='https://docs-beta.requarks.io/guide/assets#naming-restrictions', target='_blank') {{$t('editor:assets.folderNameNamingRulesLink')}}\n                    v-card-chin\n                      v-spacer\n                      v-btn(text, @click='newFolderDialog = false') {{$t('common:actions.cancel')}}\n                      v-btn.px-3(color='primary', @click='createFolder', :disabled='!isFolderNameValid', :loading='newFolderLoading') {{$t('common:actions.create')}}\n              v-toolbar(flat, dense, :color='$vuetify.theme.dark ? `grey darken-3` : `white`')\n                template(v-if='folderTree.length > 0')\n                  .body-2\n                    span.mr-1 /\n                    template(v-for='folder of folderTree')\n                      span(:key='folder.id') {{folder.name}}\n                      span.mx-1 /\n                .body-2(v-else) / #[em root]\n              template(v-if='folders.length > 0 || currentFolderId > 0')\n                v-btn.is-icon.mx-1(:color='$vuetify.theme.dark ? `grey lighten-1` : `grey darken-2`', outlined, :dark='currentFolderId > 0', @click='upFolder()', :disabled='currentFolderId === 0')\n                  v-icon mdi-folder-upload\n                v-btn.btn-normalcase.mx-1(v-for='folder of folders', :key='folder.id', depressed,  color='grey darken-2', dark, @click='downFolder(folder)')\n                  v-icon(left) mdi-folder\n                  span.caption(style='text-transform: none;') {{ folder.name }}\n                v-divider.mt-2\n              v-data-table(\n                :items='assets'\n                :headers='headers'\n                :page.sync='pagination'\n                :items-per-page='15'\n                :loading='loading'\n                must-sort,\n                sort-by='ID',\n                sort-desc,\n                hide-default-footer,\n                dense\n              )\n                template(slot='item', slot-scope='props')\n                  tr.is-clickable(\n                    @click.left='currentFileId = props.item.id'\n                    @click.right.prevent=''\n                    :class='currentFileId === props.item.id ? ($vuetify.theme.dark ? `grey darken-3-d5` : `teal lighten-5`) : ``'\n                    )\n                    td.caption(v-if='$vuetify.breakpoint.smAndUp') {{ props.item.id }}\n                    td\n                      .body-2: strong(:class='currentFileId === props.item.id ? `teal--text` : ``') {{ props.item.filename }}\n                      .caption.grey--text {{ props.item.description }}\n                    td.text-xs-center(v-if='$vuetify.breakpoint.lgAndUp')\n                      v-chip.ma-0(x-small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`')\n                        .overline {{props.item.ext.toUpperCase().substring(1)}}\n                    td.caption(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.fileSize | prettyBytes }}\n                    td.caption(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.createdAt | moment('from') }}\n                    td(v-if='$vuetify.breakpoint.smAndUp')\n                      v-menu(offset-x, min-width='200')\n                        template(v-slot:activator='{ on }')\n                          v-btn(icon, v-on='on', tile, small, @click.left='currentFileId = props.item.id')\n                            v-icon(color='grey darken-2') mdi-dots-horizontal\n                        v-list(nav, style='border-top: 5px solid #444;')\n                          //- v-list-item(@click='', disabled)\n                          //-   v-list-item-avatar(size='24')\n                          //-     v-icon(color='teal') mdi-text-short\n                          //-   v-list-item-content {{$t('common:actions.properties')}}\n                          //- template(v-if='props.item.kind === `IMAGE`')\n                          //-   v-list-item(@click='previewDialog = true', disabled)\n                          //-     v-list-item-avatar(size='24')\n                          //-       v-icon(color='green') mdi-image-search-outline\n                          //-     v-list-item-content {{$t('common:actions.preview')}}\n                          //-   v-list-item(@click='', disabled)\n                          //-     v-list-item-avatar(size='24')\n                          //-       v-icon(color='indigo') mdi-crop-rotate\n                          //-     v-list-item-content {{$t('common:actions.edit')}}\n                          //-   v-list-item(@click='', disabled)\n                          //-     v-list-item-avatar(size='24')\n                          //-       v-icon(color='purple') mdi-flash-circle\n                          //-     v-list-item-content {{$t('common:actions.optimize')}}\n                          v-list-item(@click='openRenameDialog')\n                            v-list-item-avatar(size='24')\n                              v-icon(color='orange') mdi-keyboard-outline\n                            v-list-item-content {{$t('common:actions.rename')}}\n                          //- v-list-item(@click='', disabled)\n                          //-   v-list-item-avatar(size='24')\n                          //-     v-icon(color='blue') mdi-file-move\n                          //-   v-list-item-content {{$t('common:actions.move')}}\n                          v-list-item(@click='deleteDialog = true')\n                            v-list-item-avatar(size='24')\n                              v-icon(color='red') mdi-file-hidden\n                            v-list-item-content {{$t('common:actions.delete')}}\n                template(slot='no-data')\n                  v-alert.mt-3.radius-7(icon='mdi-folder-open-outline', :value='true', outlined, color='teal') {{$t('editor:assets.folderEmpty')}}\n              .text-xs-center.py-2(v-if='this.pageTotal > 1')\n                v-pagination(v-model='pagination', :length='pageTotal', color='teal')\n              .d-flex.mt-3\n                v-toolbar.radius-7(flat, :color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-4`', dense, height='44')\n                  .body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-1` : `grey--text text--darken-1`') {{$t('editor:assets.fileCount', { count: assets.length })}}\n                v-btn.ml-3.mr-0.my-0.radius-7(color='red darken-2', large, @click='cancel', dark)\n                  v-icon(left) mdi-close\n                  span {{$t('common:actions.cancel')}}\n                v-btn.ml-3.mr-0.my-0.radius-7(color='teal', large, @click='insert', :disabled='!currentFileId', :dark='currentFileId !== null')\n                  v-icon(left) mdi-playlist-plus\n                  span {{$t('common:actions.insert')}}\n\n        v-flex(xs12, lg3)\n          v-card.radius-7.animated.fadeInRight.wait-p3s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')\n            v-card-text\n              .d-flex\n                v-toolbar.radius-7(:color='$vuetify.theme.dark ? `teal` : `teal lighten-5`', dense, flat, height='44')\n                  v-icon.mr-3(:color='$vuetify.theme.dark ? `white` : `teal`') mdi-cloud-upload\n                  .body-2(:class='$vuetify.theme.dark ? `white--text` : `teal--text`') {{$t('editor:assets.uploadAssets')}}\n                v-btn.my-0.ml-3.mr-0.radius-7(outlined, large, color='teal', @click='browse', v-if='$vuetify.breakpoint.mdAndUp')\n                  v-icon(left) mdi-plus-box-multiple\n                  span(:class='$vuetify.theme.dark ? `teal--text text--lighten-3` : ``') {{$t('common:actions.browse')}}\n              file-pond.mt-3(\n                name='mediaUpload'\n                ref='pond'\n                :label-idle='$t(`editor:assets.uploadAssetsDropZone`)'\n                allow-multiple='true'\n                :files='files'\n                max-files='10'\n                :server='filePondServerOpts'\n                :instant-upload='false'\n                :allow-revert='false'\n                @processfile='onFileProcessed'\n              )\n            v-divider\n            v-card-actions.pa-3\n              .caption.grey--text.text-darken-2 Max 10 files, 5 MB each\n              v-spacer\n              v-btn.px-4(color='teal', dark, @click='upload') {{$t('common:actions.upload')}}\n\n          //- v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')\n          //-   v-card-text.pb-0\n          //-     v-toolbar.radius-7(:color='$vuetify.theme.dark ? `teal` : `teal lighten-5`', dense, flat)\n          //-       v-icon.mr-3(:color='$vuetify.theme.dark ? `white` : `teal`') mdi-cloud-download\n          //-       .body-2(:class='$vuetify.theme.dark ? `white--text` : `teal--text`') {{$t('editor:assets.fetchImage')}}\n          //-       v-spacer\n          //-       v-chip(label, color='white', small).teal--text coming soon\n          //-     v-text-field.mt-3(\n          //-       v-model='remoteImageUrl'\n          //-       outlined\n          //-       color='teal'\n          //-       single-line\n          //-       placeholder='https://example.com/image.jpg'\n          //-     )\n          //-   v-divider\n          //-   v-card-actions.pa-3\n          //-     .caption.grey--text.text-darken-2 Max 5 MB\n          //-     v-spacer\n          //-     v-btn.px-4(color='teal', disabled) {{$t('common:actions.fetch')}}\n\n          v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')\n            v-card-text.pb-0\n              v-toolbar.radius-7(:color='$vuetify.theme.dark ? `teal` : `teal lighten-5`', dense, flat)\n                v-icon.mr-3(:color='$vuetify.theme.dark ? `white` : `teal`') mdi-format-align-top\n                .body-2(:class='$vuetify.theme.dark ? `white--text` : `teal--text`') {{$t('editor:assets.imageAlign')}}\n              v-select.mt-3(\n                v-model='imageAlignment'\n                :items='imageAlignments'\n                outlined\n                single-line\n                color='teal'\n                placeholder='None'\n              )\n\n    //- RENAME DIALOG\n\n    v-dialog(v-model='renameDialog', max-width='550', persistent)\n      v-card\n        .dialog-header.is-short.is-orange\n          v-icon.mr-2(color='white') mdi-keyboard\n          span {{$t('editor:assets.renameAsset')}}\n        v-card-text.pt-5\n          .body-2 {{$t('editor:assets.renameAssetSubtitle')}}\n          v-text-field(\n            outlined\n            single-line\n            :counter='255'\n            v-model='renameAssetName'\n            @keyup.enter='renameAsset'\n            :disabled='renameAssetLoading'\n          )\n        v-card-chin\n          v-spacer\n          v-btn(text, @click='renameDialog = false', :disabled='renameAssetLoading') {{$t('common:actions.cancel')}}\n          v-btn.px-3(color='orange darken-3', @click='renameAsset', :loading='renameAssetLoading').white--text {{$t('common:actions.rename')}}\n\n    //- DELETE DIALOG\n\n    v-dialog(v-model='deleteDialog', max-width='550', persistent)\n      v-card\n        .dialog-header.is-short.is-red\n          v-icon.mr-2(color='white') mdi-trash-can-outline\n          span {{$t('editor:assets.deleteAsset')}}\n        v-card-text.pt-5\n          .body-2 {{$t('editor:assets.deleteAssetConfirm')}}\n          .body-2.red--text.text--darken-2 {{currentAsset.filename}}?\n          .caption.mt-3 {{$t('editor:assets.deleteAssetWarn')}}\n        v-card-chin\n          v-spacer\n          v-btn(text, @click='deleteDialog = false', :disabled='deleteAssetLoading') {{$t('common:actions.cancel')}}\n          v-btn.px-3(color='red darken-2', @click='deleteAsset', :loading='deleteAssetLoading').white--text {{$t('common:actions.delete')}}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { get, sync } from 'vuex-pathify'\nimport Cookies from 'js-cookie'\nimport vueFilePond from 'vue-filepond'\nimport 'filepond/dist/filepond.min.css'\n\nimport listAssetQuery from 'gql/editor/editor-media-query-list.gql'\nimport listFolderAssetQuery from 'gql/editor/editor-media-query-folder-list.gql'\nimport createAssetFolderMutation from 'gql/editor/editor-media-mutation-folder-create.gql'\nimport renameAssetMutation from 'gql/editor/editor-media-mutation-asset-rename.gql'\nimport deleteAssetMutation from 'gql/editor/editor-media-mutation-asset-delete.gql'\n\nconst FilePond = vueFilePond()\nconst localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i\nconst disallowedFolderChars = /[A-Z()=.!@#$%?&*+`~<>,;:\\\\/[\\]¬{| ]/\n\nexport default {\n  components: {\n    FilePond\n  },\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      folders: [],\n      files: [],\n      assets: [],\n      pagination: 1,\n      remoteImageUrl: '',\n      imageAlignments: [\n        { text: 'None', value: '' },\n        { text: 'Left', value: 'left' },\n        { text: 'Centered', value: 'center' },\n        { text: 'Right', value: 'right' },\n        { text: 'Absolute Top Right', value: 'abstopright' }\n      ],\n      imageAlignment: '',\n      loading: false,\n      newFolderDialog: false,\n      newFolderName: '',\n      newFolderLoading: false,\n      previewDialog: false,\n      renameDialog: false,\n      renameAssetName: '',\n      renameAssetLoading: false,\n      deleteDialog: false,\n      deleteAssetLoading: false\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    editorKey: get('editor/editorKey'),\n    activeModal: sync('editor/activeModal'),\n    folderTree: get('editor/media@folderTree'),\n    currentFolderId: sync('editor/media@currentFolderId'),\n    currentFileId: sync('editor/media@currentFileId'),\n    pageTotal () {\n      if (!this.assets) {\n        return 0\n      }\n\n      return Math.ceil(this.assets.length / 15)\n    },\n    headers() {\n      return _.compact([\n        this.$vuetify.breakpoint.smAndUp && { text: this.$t('editor:assets.headerId'), value: 'id', width: 80 },\n        { text: this.$t('editor:assets.headerFilename'), value: 'filename' },\n        this.$vuetify.breakpoint.lgAndUp && { text: this.$t('editor:assets.headerType'), value: 'ext', width: 90 },\n        this.$vuetify.breakpoint.mdAndUp && { text: this.$t('editor:assets.headerFileSize'), value: 'fileSize', width: 110 },\n        this.$vuetify.breakpoint.mdAndUp && { text: this.$t('editor:assets.headerAdded'), value: 'createdAt', width: 175 },\n        this.$vuetify.breakpoint.smAndUp && { text: this.$t('editor:assets.headerActions'), value: '', width: 80, sortable: false, align: 'right' }\n      ])\n    },\n    isFolderNameValid() {\n      return this.newFolderName.length > 1 && !localeSegmentRegex.test(this.newFolderName) && !disallowedFolderChars.test(this.newFolderName)\n    },\n    currentAsset () {\n      return _.find(this.assets, ['id', this.currentFileId]) || {}\n    },\n    filePondServerOpts () {\n      const jwtToken = Cookies.get('jwt')\n      return {\n        process: {\n          url: '/u',\n          headers: {\n            'Authorization': `Bearer ${jwtToken}`\n          }\n        }\n      }\n    }\n  },\n  watch: {\n    newFolderDialog(newValue, oldValue) {\n      if (newValue) {\n        this.$nextTick(() => {\n          this.$refs.folderNameIpt.focus()\n        })\n      }\n    }\n  },\n  filters: {\n    prettyBytes(num) {\n      if (typeof num !== 'number' || isNaN(num)) {\n        throw new TypeError('Expected a number')\n      }\n\n      let exponent\n      let unit\n      let neg = num < 0\n      let units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\n      if (neg) {\n        num = -num\n      }\n      if (num < 1) {\n        return (neg ? '-' : '') + num + ' B'\n      }\n      exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), units.length - 1)\n      num = (num / Math.pow(1000, exponent)).toFixed(2) * 1\n      unit = units[exponent]\n\n      return (neg ? '-' : '') + num + ' ' + unit\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.assets.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('editor:assets.refreshSuccess'),\n        style: 'success',\n        icon: 'check'\n      })\n    },\n    insert () {\n      const asset = _.find(this.assets, ['id', this.currentFileId])\n      const assetPath = this.folderTree.map(f => f.slug).join('/')\n      this.$root.$emit('editorInsert', {\n        kind: asset.kind,\n        path: this.currentFolderId > 0 ? `/${assetPath}/${asset.filename}` : `/${asset.filename}`,\n        text: asset.filename,\n        align: this.imageAlignment\n      })\n      this.activeModal = ''\n    },\n    browse () {\n      this.$refs.pond.browse()\n    },\n    async upload () {\n      const files = this.$refs.pond.getFiles()\n      if (files.length < 1) {\n        return this.$store.commit('showNotification', {\n          message: this.$t('editor:assets.noUploadError'),\n          style: 'warning',\n          icon: 'warning'\n        })\n      }\n      for (let file of files) {\n        file.setMetadata({\n          folderId: this.currentFolderId\n        })\n      }\n      await this.$refs.pond.processFiles()\n    },\n    async onFileProcessed (err, file) {\n      if (err) {\n        return this.$store.commit('showNotification', {\n          message: this.$t('editor:assets.uploadFailed'),\n          style: 'error',\n          icon: 'error'\n        })\n      }\n      _.delay(() => {\n        this.$refs.pond.removeFile(file.id)\n      }, 5000)\n\n      await this.$apollo.queries.assets.refetch()\n    },\n    downFolder(folder) {\n      this.$store.commit('editor/pushMediaFolderTree', folder)\n      this.currentFolderId = folder.id\n      this.currentFileId = null\n    },\n    upFolder() {\n      this.$store.commit('editor/popMediaFolderTree')\n      const parentFolder = _.last(this.folderTree)\n      this.currentFolderId = parentFolder ? parentFolder.id : 0\n      this.currentFileId = null\n    },\n    async createFolder() {\n      this.$store.commit(`loadingStart`, 'editor-media-createfolder')\n      this.newFolderLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: createAssetFolderMutation,\n          variables: {\n            parentFolderId: this.currentFolderId,\n            slug: this.newFolderName\n          }\n        })\n        if (_.get(resp, 'data.assets.createFolder.responseResult.succeeded', false)) {\n          await this.$apollo.queries.folders.refetch()\n          this.$store.commit('showNotification', {\n            message: this.$t('editor:assets.folderCreateSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n          this.newFolderDialog = false\n          this.newFolderName = ''\n        } else {\n          this.$store.commit('pushGraphError', new Error(_.get(resp, 'data.assets.createFolder.responseResult.message')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.newFolderLoading = false\n      this.$store.commit(`loadingStop`, 'editor-media-createfolder')\n    },\n    openRenameDialog() {\n      this.renameAssetName = this.currentAsset.filename\n      this.renameDialog = true\n    },\n    async renameAsset() {\n      this.$store.commit(`loadingStart`, 'editor-media-renameasset')\n      this.renameAssetLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: renameAssetMutation,\n          variables: {\n            id: this.currentFileId,\n            filename: this.renameAssetName\n          }\n        })\n        if (_.get(resp, 'data.assets.renameAsset.responseResult.succeeded', false)) {\n          await this.$apollo.queries.assets.refetch()\n          this.$store.commit('showNotification', {\n            message: this.$t('editor:assets.renameSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n          this.renameDialog = false\n          this.renameAssetName = ''\n        } else {\n          this.$store.commit('pushGraphError', new Error(_.get(resp, 'data.assets.renameAsset.responseResult.message')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.renameAssetLoading = false\n      this.$store.commit(`loadingStop`, 'editor-media-renameasset')\n    },\n    async deleteAsset() {\n      this.$store.commit(`loadingStart`, 'editor-media-deleteasset')\n      this.deleteAssetLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: deleteAssetMutation,\n          variables: {\n            id: this.currentFileId\n          }\n        })\n        if (_.get(resp, 'data.assets.deleteAsset.responseResult.succeeded', false)) {\n          this.currentFileId = null\n          await this.$apollo.queries.assets.refetch()\n          this.$store.commit('showNotification', {\n            message: this.$t('editor:assets.deleteSuccess'),\n            style: 'success',\n            icon: 'check'\n          })\n          this.deleteDialog = false\n        } else {\n          this.$store.commit('pushGraphError', new Error(_.get(resp, 'data.assets.deleteAsset.responseResult.message')))\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n      this.deleteAssetLoading = false\n      this.$store.commit(`loadingStop`, 'editor-media-deleteasset')\n    },\n    cancel () {\n      this.activeModal = ''\n    }\n  },\n  apollo: {\n    folders: {\n      query: listFolderAssetQuery,\n      variables() {\n        return {\n          parentFolderId: this.currentFolderId\n        }\n      },\n      fetchPolicy: 'network-only',\n      update: (data) => data.assets.folders,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'editor-media-folders-list-refresh')\n      }\n    },\n    assets: {\n      query: listAssetQuery,\n      variables() {\n        return {\n          folderId: this.currentFolderId,\n          kind: 'ALL'\n        }\n      },\n      throttle: 1000,\n      fetchPolicy: 'network-only',\n      update: (data) => data.assets.list,\n      watchLoading (isLoading) {\n        this.loading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'editor-media-list-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.editor-modal-media {\n  position: fixed !important;\n  top: 112px;\n  left: 64px;\n  z-index: 10;\n  width: calc(100vw - 64px - 17px);\n  height: calc(100vh - 112px - 24px);\n  background-color: rgba(darken(mc('grey', '900'), 3%), .9) !important;\n  overflow: auto;\n\n  @include until($tablet) {\n    left: 40px;\n    width: calc(100vw - 40px);\n    height: calc(100vh - 112px - 24px);\n  }\n\n  &.is-editor-ckeditor {\n    top: 64px;\n    left: 0;\n    width: 100%;\n    height: calc(100vh - 64px - 26px);\n\n    @include until($tablet) {\n      top: 56px;\n      left: 0;\n      width: 100%;\n      height: calc(100vh - 56px - 24px);\n    }\n  }\n\n  &.is-editor-code {\n    top: 64px;\n    height: calc(100vh - 64px - 26px);\n\n    @include until($tablet) {\n      top: 56px;\n      height: calc(100vh - 56px - 24px);\n    }\n  }\n\n  &.is-editor-common {\n    top: 64px;\n    left: 0;\n    width: 100%;\n    height: calc(100vh - 64px);\n\n    @include until($tablet) {\n      top: 56px;\n      left: 0;\n      width: 100%;\n      height: calc(100vh - 56px);\n    }\n  }\n\n  .filepond--root {\n    margin-bottom: 0;\n  }\n\n  .filepond--drop-label {\n    cursor: pointer;\n\n    > label {\n      cursor: pointer;\n    }\n  }\n\n  .filepond--file-action-button.filepond--action-process-item {\n    display: none;\n  }\n\n  .v-btn--icon {\n    padding: 0 20px;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-properties.vue",
    "content": "<template lang='pug'>\n  v-dialog(\n    v-model='isShown'\n    persistent\n    width='1000'\n    :fullscreen='$vuetify.breakpoint.smAndDown'\n    )\n    .dialog-header\n      v-icon(color='white') mdi-tag-text-outline\n      .subtitle-1.white--text.ml-3 {{$t('editor:props.pageProperties')}}\n      v-spacer\n      v-btn.mx-0(\n        outlined\n        dark\n        @click.native='close'\n        )\n        v-icon(left) mdi-check\n        span {{ $t('common:actions.ok') }}\n    v-card(tile)\n      v-tabs(color='white', background-color='blue darken-1', dark, centered, v-model='currentTab')\n        v-tab {{$t('editor:props.info')}}\n        v-tab {{$t('editor:props.scheduling')}}\n        v-tab(:disabled='!hasScriptPermission') {{$t('editor:props.scripts')}}\n        //- v-tab(disabled) {{$t('editor:props.social')}}\n        v-tab(:disabled='!hasStylePermission') {{$t('editor:props.styles')}}\n        v-tab-item(transition='fade-transition', reverse-transition='fade-transition')\n          v-card-text.pt-5\n            .overline.pb-5 {{$t('editor:props.pageInfo')}}\n            v-text-field(\n              ref='iptTitle'\n              outlined\n              :label='$t(`editor:props.title`)'\n              counter='255'\n              v-model='title'\n              )\n            v-text-field(\n              outlined\n              :label='$t(`editor:props.shortDescription`)'\n              counter='255'\n              v-model='description'\n              persistent-hint\n              :hint='$t(`editor:props.shortDescriptionHint`)'\n              )\n          v-divider\n          v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d3` : `lighten-5`')\n            .overline.pb-5 {{$t('editor:props.path')}}\n            v-container.pa-0(fluid, grid-list-lg)\n              v-layout(row, wrap)\n                v-flex(xs12, md2)\n                  v-select(\n                    outlined\n                    :label='$t(`editor:props.locale`)'\n                    suffix='/'\n                    :items='namespaces'\n                    v-model='locale'\n                    hide-details\n                  )\n                v-flex(xs12, md10)\n                  v-text-field(\n                    outlined\n                    :label='$t(`editor:props.path`)'\n                    append-icon='mdi-folder-search'\n                    v-model='path'\n                    :hint='$t(`editor:props.pathHint`)'\n                    persistent-hint\n                    @click:append='showPathSelector'\n                    :rules='[rules.required, rules.path]'\n                    )\n          v-divider\n          v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-4`')\n            .overline.pb-5 {{$t('editor:props.categorization')}}\n            v-chip-group.radius-5.mb-5(column, v-if='tags && tags.length > 0')\n              v-chip(\n                v-for='tag of tags'\n                :key='`tag-` + tag'\n                close\n                label\n                color='teal'\n                text-color='teal lighten-5'\n                @click:close='removeTag(tag)'\n                ) {{tag}}\n            v-combobox(\n              :label='$t(`editor:props.tags`)'\n              outlined\n              v-model='newTag'\n              :hint='$t(`editor:props.tagsHint`)'\n              :items='newTagSuggestions'\n              :loading='$apollo.queries.newTagSuggestions.loading'\n              persistent-hint\n              hide-no-data\n              :search-input.sync='newTagSearch'\n              )\n        v-tab-item(transition='fade-transition', reverse-transition='fade-transition')\n          v-card-text\n            .overline {{$t('editor:props.publishState')}}\n            v-switch(\n              :label='$t(`editor:props.publishToggle`)'\n              v-model='isPublished'\n              color='primary'\n              :hint='$t(`editor:props.publishToggleHint`)'\n              persistent-hint\n              inset\n              )\n          v-divider\n          v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d3` : `lighten-5`')\n            v-container.pa-0(fluid, grid-list-lg)\n              v-row\n                v-col(cols='6')\n                  v-dialog(\n                    ref='menuPublishStart'\n                    :close-on-content-click='false'\n                    v-model='isPublishStartShown'\n                    :return-value.sync='publishStartDate'\n                    width='460px'\n                    :disabled='!isPublished'\n                    )\n                    template(v-slot:activator='{ on }')\n                      v-text-field(\n                        v-on='on'\n                        :label='$t(`editor:props.publishStart`)'\n                        v-model='publishStartDate'\n                        prepend-icon='mdi-calendar-check'\n                        readonly\n                        outlined\n                        clearable\n                        :hint='$t(`editor:props.publishStartHint`)'\n                        persistent-hint\n                        :disabled='!isPublished'\n                        )\n                    v-date-picker(\n                      v-model='publishStartDate'\n                      :min='(new Date()).toISOString().substring(0, 10)'\n                      color='primary'\n                      reactive\n                      scrollable\n                      landscape\n                      )\n                      v-spacer\n                      v-btn(\n                        text\n                        color='primary'\n                        @click='isPublishStartShown = false'\n                        ) {{$t('common:actions.cancel')}}\n                      v-btn(\n                        text\n                        color='primary'\n                        @click='$refs.menuPublishStart.save(publishStartDate)'\n                        ) {{$t('common:actions.ok')}}\n                v-col(cols='6')\n                  v-dialog(\n                    ref='menuPublishEnd'\n                    :close-on-content-click='false'\n                    v-model='isPublishEndShown'\n                    :return-value.sync='publishEndDate'\n                    width='460px'\n                    :disabled='!isPublished'\n                    )\n                    template(v-slot:activator='{ on }')\n                      v-text-field(\n                        v-on='on'\n                        :label='$t(`editor:props.publishEnd`)'\n                        v-model='publishEndDate'\n                        prepend-icon='mdi-calendar-remove'\n                        readonly\n                        outlined\n                        clearable\n                        :hint='$t(`editor:props.publishEndHint`)'\n                        persistent-hint\n                        :disabled='!isPublished'\n                        )\n                    v-date-picker(\n                      v-model='publishEndDate'\n                      :min='(new Date()).toISOString().substring(0, 10)'\n                      color='primary'\n                      reactive\n                      scrollable\n                      landscape\n                      )\n                      v-spacer\n                      v-btn(\n                        text\n                        color='primary'\n                        @click='isPublishEndShown = false'\n                        ) {{$t('common:actions.cancel')}}\n                      v-btn(\n                        text\n                        color='primary'\n                        @click='$refs.menuPublishEnd.save(publishEndDate)'\n                        ) {{$t('common:actions.ok')}}\n\n        v-tab-item(:transition='false', :reverse-transition='false')\n          .editor-props-codeeditor-title\n            .overline {{$t('editor:props.html')}}\n          .editor-props-codeeditor\n            textarea(ref='codejs')\n          .editor-props-codeeditor-hint\n            .caption {{$t('editor:props.htmlHint')}}\n\n        //- v-tab-item(transition='fade-transition', reverse-transition='fade-transition')\n        //-   v-card-text\n        //-     .overline {{$t('editor:props.socialFeatures')}}\n        //-     v-switch(\n        //-       :label='$t(`editor:props.allowComments`)'\n        //-       v-model='isPublished'\n        //-       color='primary'\n        //-       :hint='$t(`editor:props.allowCommentsHint`)'\n        //-       persistent-hint\n        //-       inset\n        //-       )\n        //-     v-switch(\n        //-       :label='$t(`editor:props.allowRatings`)'\n        //-       v-model='isPublished'\n        //-       color='primary'\n        //-       :hint='$t(`editor:props.allowRatingsHint`)'\n        //-       persistent-hint\n        //-       disabled\n        //-       inset\n        //-       )\n        //-     v-switch(\n        //-       :label='$t(`editor:props.displayAuthor`)'\n        //-       v-model='isPublished'\n        //-       color='primary'\n        //-       :hint='$t(`editor:props.displayAuthorHint`)'\n        //-       persistent-hint\n        //-       inset\n        //-       )\n        //-     v-switch(\n        //-       :label='$t(`editor:props.displaySharingBar`)'\n        //-       v-model='isPublished'\n        //-       color='primary'\n        //-       :hint='$t(`editor:props.displaySharingBarHint`)'\n        //-       persistent-hint\n        //-       inset\n        //-       )\n\n        v-tab-item(:transition='false', :reverse-transition='false')\n          .editor-props-codeeditor-title\n            .overline {{$t('editor:props.css')}}\n          .editor-props-codeeditor\n            textarea(ref='codecss')\n          .editor-props-codeeditor-hint\n            .caption {{$t('editor:props.cssHint')}}\n\n    page-selector(:mode='pageSelectorMode', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath')\n</template>\n\n<script>\nimport _ from 'lodash'\nimport { sync, get } from 'vuex-pathify'\nimport gql from 'graphql-tag'\n\nimport CodeMirror from 'codemirror'\nimport 'codemirror/lib/codemirror.css'\nimport 'codemirror/mode/htmlmixed/htmlmixed.js'\nimport 'codemirror/mode/css/css.js'\n\n/* global siteLangs, siteConfig */\nconst filenamePattern = /^(?![\\#\\/\\.\\$\\^\\=\\*\\;\\:\\&\\?\\(\\)\\[\\]\\{\\}\\\"\\'\\>\\<\\,\\@\\!\\%\\`\\~\\s])(?!.*[\\#\\/\\.\\$\\^\\=\\*\\;\\:\\&\\?\\(\\)\\[\\]\\{\\}\\\"\\'\\>\\<\\,\\@\\!\\%\\`\\~\\s]$)[^\\#\\.\\$\\^\\=\\*\\;\\:\\&\\?\\(\\)\\[\\]\\{\\}\\\"\\'\\>\\<\\,\\@\\!\\%\\`\\~\\s]*$/\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data () {\n    return {\n      isPublishStartShown: false,\n      isPublishEndShown: false,\n      pageSelectorShown: false,\n      namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],\n      newTag: '',\n      newTagSuggestions: [],\n      newTagSearch: '',\n      currentTab: 0,\n      cm: null,\n      rules: {\n        required: value => !!value || 'This field is required.',\n        path: value => {\n          return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'\n        }\n      }\n    }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    },\n    mode: get('editor/mode'),\n    title: sync('page/title'),\n    description: sync('page/description'),\n    locale: sync('page/locale'),\n    tags: sync('page/tags'),\n    path: sync('page/path'),\n    isPublished: sync('page/isPublished'),\n    publishStartDate: sync('page/publishStartDate'),\n    publishEndDate: sync('page/publishEndDate'),\n    scriptJs: sync('page/scriptJs'),\n    scriptCss: sync('page/scriptCss'),\n    hasScriptPermission: get('page/effectivePermissions@pages.script'),\n    hasStylePermission: get('page/effectivePermissions@pages.style'),\n    pageSelectorMode () {\n      return (this.mode === 'create') ? 'create' : 'move'\n    }\n  },\n  watch: {\n    value (newValue, oldValue) {\n      if (newValue) {\n        _.delay(() => {\n          this.$refs.iptTitle.focus()\n        }, 500)\n      }\n    },\n    newTag (newValue, oldValue) {\n      const tagClean = _.trim(newValue || '').toLowerCase()\n      if (tagClean && tagClean.length > 0) {\n        if (!_.includes(this.tags, tagClean)) {\n          this.tags = [...this.tags, tagClean]\n        }\n        this.$nextTick(() => {\n          this.newTag = null\n        })\n      }\n    },\n    currentTab (newValue, oldValue) {\n      if (this.cm) {\n        this.cm.toTextArea()\n      }\n      if (newValue === 2) {\n        this.$nextTick(() => {\n          setTimeout(() => {\n            this.loadEditor(this.$refs.codejs, 'html')\n          }, 100)\n        })\n      } else if (newValue === 3) {\n        this.$nextTick(() => {\n          setTimeout(() => {\n            this.loadEditor(this.$refs.codecss, 'css')\n          }, 100)\n        })\n      }\n    }\n  },\n  methods: {\n    removeTag (tag) {\n      this.tags = _.without(this.tags, tag)\n    },\n    close() {\n      this.isShown = false\n    },\n    showPathSelector() {\n      this.pageSelectorShown = true\n    },\n    setPath({ path, locale }) {\n      this.locale = locale\n      this.path = path\n    },\n    loadEditor(ref, mode) {\n      this.cm = CodeMirror.fromTextArea(ref, {\n        tabSize: 2,\n        mode: `text/${mode}`,\n        theme: 'wikijs-dark',\n        lineNumbers: true,\n        lineWrapping: true,\n        line: true,\n        styleActiveLine: true,\n        viewportMargin: 50,\n        inputStyle: 'contenteditable',\n        direction: 'ltr'\n      })\n      switch (mode) {\n        case 'html':\n          this.cm.setValue(this.scriptJs)\n          this.cm.on('change', c => {\n            this.scriptJs = c.getValue()\n          })\n          break\n        case 'css':\n          this.cm.setValue(this.scriptCss)\n          this.cm.on('change', c => {\n            this.scriptCss = c.getValue()\n          })\n          break\n        default:\n          console.warn('Invalid Editor Mode')\n          break\n      }\n      this.cm.setSize(null, '500px')\n      this.$nextTick(() => {\n        this.cm.refresh()\n        this.cm.focus()\n      })\n    }\n  },\n  apollo: {\n    newTagSuggestions: {\n      query: gql`\n        query ($query: String!) {\n          pages {\n            searchTags (query: $query)\n          }\n        }\n      `,\n      variables () {\n        return {\n          query: this.newTagSearch\n        }\n      },\n      fetchPolicy: 'cache-first',\n      update: (data) => _.get(data, 'pages.searchTags', []),\n      skip () {\n        return !this.value || _.isEmpty(this.newTagSearch)\n      },\n      throttle: 500\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.editor-props-codeeditor {\n  background-color: mc('grey', '900');\n  min-height: 500px;\n\n  > textarea {\n    visibility: hidden;\n  }\n\n  &-title {\n    background-color: mc('grey', '900');\n    border-bottom: 1px solid lighten(mc('grey', '900'), 10%);\n    color: #FFF;\n    padding: 10px;\n  }\n\n  &-hint {\n    background-color: mc('grey', '900');\n    border-top: 1px solid lighten(mc('grey', '900'), 5%);\n    color: mc('grey', '500');\n    padding: 5px 10px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/editor/editor-modal-unsaved.vue",
    "content": "<template lang=\"pug\">\n  v-dialog(v-model='isShown', max-width='550')\n    v-card\n      .dialog-header.is-short.is-red\n        v-icon.mr-2(color='white') mdi-alert\n        span {{$t('editor:unsaved.title')}}\n      v-card-text.pt-4\n        .body-2 {{$t('editor:unsaved.body')}}\n      v-card-chin\n        v-spacer\n        v-btn(text, @click='isShown = false') {{$t('common:actions.cancel')}}\n        v-btn.px-4(color='red', @click='discard', dark) {{$t('common:actions.discardChanges')}}\n</template>\n\n<script>\n\nexport default {\n  props: {\n    value: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return { }\n  },\n  computed: {\n    isShown: {\n      get() { return this.value },\n      set(val) { this.$emit('input', val) }\n    }\n  },\n  methods: {\n    async discard() {\n      this.isShown = false\n      this.$emit('discard', true)\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/components/editor/editor-redirect.vue",
    "content": "<template lang='pug'>\n  .editor-redirect\n    .editor-redirect-main\n      .editor-redirect-editor\n        v-container.px-2.pt-1(fluid)\n          v-row(dense)\n            v-col(\n              cols='12'\n              lg='8'\n              offset-lg='2'\n              xl='6'\n              offset-xl='3'\n              )\n              v-card.pt-2\n                v-card-text\n                  .pb-1\n                    .subtitle-2.primary--text When a user reaches this page\n                    .caption.grey--text.text--darken-1 and matches one of these rules...\n                  v-timeline(dense)\n                    v-slide-x-reverse-transition(group, hide-on-leave)\n                      v-timeline-item(\n                        key='cond-add-new'\n                        hide-dot\n                        )\n                        v-btn(\n                          color='primary'\n                          @click=''\n                          )\n                          v-icon(left) mdi-plus\n                          span Add Conditional Rule\n                      v-timeline-item(\n                        key='cond-none'\n                        small\n                        color='grey'\n                        )\n                        v-card.grey.lighten-5(flat)\n                          v-card-text\n                            .body-2: strong No conditional rule\n                            em Add conditional rules to direct users to a different page based on their group.\n                      v-timeline-item(\n                        key='cond-rule-1'\n                        small\n                        color='primary'\n                        )\n                        v-card.blue-grey.lighten-5(flat)\n                          v-card-text\n                            .d-flex.align-center\n                              .body-2: strong User is a member of any of these groups:\n                              v-select.ml-3(\n                                color='primary'\n                                :items='groups'\n                                item-text='name'\n                                item-value='id'\n                                multiple\n                                solo\n                                flat\n                                hide-details\n                                dense\n                                chips\n                                small-chips\n                                )\n                            v-divider.my-3\n                            .d-flex.align-center\n                              .body-2.mr-3 then redirect to\n                              v-btn-toggle.mr-3(\n                                v-model='fallbackMode'\n                                mandatory\n                                color='primary'\n                                borderless\n                                dense\n                                )\n                                v-btn.text-none(value='page') Page\n                                v-btn.text-none(value='url') External URL\n                              v-btn.mr-3(\n                                v-if='fallbackMode === `page`'\n                                color='primary'\n                                )\n                                v-icon(left) mdi-magnify\n                                span Select Page...\n                              v-text-field(\n                                v-if='fallbackMode === `url`'\n                                label='External URL'\n                                outlined\n                                hint='Required - Title of the API'\n                                hide-details\n                                v-model='fallbackUrl'\n                                dense\n                                single-line\n                              )\n                  v-divider.mb-5\n                  .subtitle-2.primary--text Otherwise, redirect to...\n                  .caption.grey--text.text--darken-1.pb-2 This fallback rule is mandatory and used if none of the conditional rules above applies.\n                  .d-flex.align-center\n                    v-btn-toggle.mr-3(\n                      v-model='fallbackMode'\n                      mandatory\n                      color='primary'\n                      borderless\n                      dense\n                      )\n                      v-btn.text-none(value='page') Page\n                      v-btn.text-none(value='url') External URL\n                    v-btn.mr-3(\n                      v-if='fallbackMode === `page`'\n                      color='primary'\n                      )\n                      v-icon(left) mdi-magnify\n                      span Select Page...\n                    v-text-field(\n                      v-if='fallbackMode === `url`'\n                      label='External URL'\n                      outlined\n                      hint='Required - Title of the API'\n                      hide-details\n                      v-model='fallbackUrl'\n                      dense\n                      single-line\n                    )\n\n    v-system-bar.editor-redirect-sysbar(dark, status, color='grey darken-3')\n      .caption.editor-redirect-sysbar-locale {{locale.toUpperCase()}}\n      .caption.px-3 /{{path}}\n      template(v-if='$vuetify.breakpoint.mdAndUp')\n        v-spacer\n        .caption Redirect\n        v-spacer\n        .caption 0 rules\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { v4 as uuid } from 'uuid'\nimport { get, sync } from 'vuex-pathify'\n\nexport default {\n  data() {\n    return {\n      fallbackMode: 'page',\n      fallbackUrl: 'https://'\n    }\n  },\n  computed: {\n    isMobile() {\n      return this.$vuetify.breakpoint.smAndDown\n    },\n    locale: get('page/locale'),\n    path: get('page/path'),\n    mode: get('editor/mode'),\n    activeModal: sync('editor/activeModal')\n  },\n  methods: {\n  },\n  mounted() {\n    this.$store.set('editor/editorKey', 'redirect')\n\n    if (this.mode === 'create') {\n      this.$store.set('editor/content', '<h1>Title</h1>\\n\\n<p>Some text here</p>')\n    }\n  },\n  apollo: {\n    groups: {\n      query: gql`\n        {\n          groups {\n            list {\n              id\n              name\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => data.groups.list,\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'editor-redirect-groups')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n$editor-height: calc(100vh - 64px - 24px);\n$editor-height-mobile: calc(100vh - 56px - 16px);\n\n.editor-redirect {\n  &-main {\n    display: flex;\n    width: 100%;\n  }\n\n  &-editor {\n    background-color: darken(mc('grey', '100'), 4.5%);\n    flex: 1 1 50%;\n    display: block;\n    height: $editor-height;\n    position: relative;\n\n    @at-root .theme--dark & {\n      background-color: darken(mc('grey', '900'), 4.5%);\n    }\n  }\n\n  &-sidebar {\n    width: 200px;\n  }\n\n  &-sysbar {\n    padding-left: 0 !important;\n\n    &-locale {\n      background-color: rgba(255,255,255,.25);\n      display:inline-flex;\n      padding: 0 12px;\n      height: 24px;\n      width: 63px;\n      justify-content: center;\n      align-items: center;\n    }\n  }\n\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/markdown/help.vue",
    "content": "<template lang='pug'>\n  v-card.editor-markdown-help.animated.fadeInLeft(flat, tile)\n    v-container.pa-3(grid-list-lg, fluid)\n      v-layout(row, wrap)\n        v-flex(xs12, lg6, xl4)\n          v-card.radius-7.animated.fadeInUp(light)\n            v-card-text\n              .d-flex\n                v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44')\n                  v-icon.mr-3(color='teal') mdi-information-variant\n                  .body-2.teal--text Markdown Reference\n              .body-2.mt-3 Bold\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div **Lorem ipsum**\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption: strong Lorem ipsum\n              .body-2.mt-3 Italic\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div *Lorem ipsum*\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption: em Lorem ipsum\n              .body-2.mt-3 Strikethrough\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div ~~Lorem ipsum~~\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption(style='text-decoration: line-through;') Lorem ipsum\n              .body-2.mt-3 Headers\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div # Header 1\n                      div ## Header 2\n                      div ### Header 3\n                      div #### Header 4\n                      div ##### Header 5\n                      div ###### Header 6\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      div(style='font-weight: 700; font-size: 24px;') Header 1\n                      div(style='font-weight: 700; font-size: 22px;') Header 2\n                      div(style='font-weight: 700; font-size: 20px;') Header 3\n                      div(style='font-weight: 700; font-size: 18px;') Header 4\n                      div(style='font-weight: 700; font-size: 16px;') Header 5\n                      div(style='font-weight: 700; font-size: 14px;') Header 6\n              .body-2.mt-3 Unordered Lists\n              .caption.grey--text.text--darken-1: em You can also use the asterisk symbol instead of the dash.\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div - Unordered List Item 1\n                      div - Unordered List Item 2\n                      div - Unordered List Item 3\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      ul\n                        li Unordered List Item 1\n                        li Unordered List Item 2\n                        li Unordered List Item 3\n              .body-2.mt-3 Ordered Lists\n              .caption.grey--text.text--darken-1: em Even though we prefix all lines with #[strong 1.], the output will be correctly numbered automatically.\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div 1. Ordered List Item 1\n                      div 1. Ordered List Item 2\n                      div 1. Ordered List Item 3\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      ol\n                        li Unordered List Item 1\n                        li Unordered List Item 2\n                        li Unordered List Item 3\n              .body-2.mt-3 Images\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div ![Caption Text](/path/to/image.jpg)\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      img(src='https://via.placeholder.com/150x50.png')\n        v-flex(xs12, lg6, xl4)\n          v-card.radius-7.animated.fadeInUp.wait-p1s(light)\n            v-card-text\n              .d-flex\n                v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44')\n                  v-icon.mr-3(color='teal') mdi-information-variant\n                  .body-2.teal--text Markdown Reference (continued)\n              .body-2.mt-3 Links\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div [Link Text](https://wiki.js.org)\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption: a(href='https://wiki.js.org', target='_blank') Link Text\n              .body-2.mt-3 Superscript\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div Lorem ^ipsum^\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption Lorem #[sup ipsum]\n              .body-2.mt-3 Subscript\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div Lorem ~ipsum~\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption: em Lorem #[sub ipsum]\n              .body-2.mt-3 Horizontal Line\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div Lorem ipsum\n                      div ---\n                      div Dolor sit amet\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption Lorem ipsum\n                      v-divider.my-2\n                      .caption Dolor sit amet\n              .body-2.mt-3 Inline Code\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div Lorem `ipsum dolor sit` amet\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      .caption Lorem #[code ipsum dolor sit] amet\n              .body-2.mt-3 Code Blocks\n              .caption.grey--text.text--darken-1: em In the example below, #[strong js] defines the syntax highlighting language to use. It can be omitted.\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div ```js\n                      div function main () {\n                      div.pl-3 echo 'Lorem ipsum'\n                      div }\n                      div ```\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text.contents\n                      pre.prismjs.line-numbers.language-js\n                        code.language-js\n                          span.token.keyword function\n                          span.token.function  main\n                          span.token.punctuation  (\n                          span.token.punctuation )\n                          span.token.punctuation  {#[br]\n                          |   echo\n                          span.token.string  'Lorem ipsum'#[br]\n                          span.token.punctuation }\n                          span.line-numbers-rows(aria-hidden='true')\n                            span\n                            span\n                            span\n              .body-2.mt-3 Blockquotes\n              v-layout(row)\n                v-flex(xs6)\n                  v-card.editor-markdown-help-source(flat)\n                    v-card-text\n                      div &gt; Lorem ipsum\n                      div &gt; dolor sit amet\n                      div &gt; consectetur adipiscing elit\n                v-icon mdi-chevron-right\n                v-flex\n                  v-card.editor-markdown-help-result(flat)\n                    v-card-text\n                      blockquote(style='border: 1px solid #263238; border-radius: .5rem; padding: 1rem 24px;') Lorem ipsum#[br]dolor sit amet#[br]consectetur adipiscing elit\n\n        v-flex(xs12, xl4)\n          v-card.radius-7.animated.fadeInUp.wait-p2s(light)\n            v-card-text\n              v-toolbar.radius-7(color='teal lighten-5', dense, flat)\n                v-icon.mr-3(color='teal') mdi-keyboard\n                .body-2.teal--text Keyboard Shortcuts\n              v-list.editor-markdown-help-kbd(two-line, dense)\n                v-list-item\n                  v-list-item-content.body-2 Bold\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd B]\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Italic\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd I]\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Increase Header Level\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd {{altKey}}] + #[kbd Right]\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Decrease Header Level\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd {{altKey}}] + #[kbd Left]\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Save\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd S]\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Undo\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd Z]\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Redo\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd Y]\n                v-divider\n                v-list-item\n                  v-list-item-content\n                    v-list-item-title.body-2 Distraction Free Mode\n                    v-list-item-subtitle Press <kbd>Esc</kbd> to exit.\n                  v-list-item-action #[kbd F11]\n\n          v-card.radius-7.animated.fadeInUp.wait-p3s.mt-3(light)\n            v-card-text\n              v-toolbar.radius-7(color='teal lighten-5', dense, flat)\n                v-icon.mr-3(color='teal') mdi-mouse\n                .body-2.teal--text Multi-Selection\n              v-list.editor-markdown-help-kbd(two-line, dense)\n                v-list-item\n                  v-list-item-content.body-2 Multiple Cursors\n                  v-list-item-action #[kbd {{ctrlKey}}] + Left Click\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Select Region\n                  v-list-item-action #[kbd {{ctrlKey}}] + #[kbd {{altKey}}] + Left Click\n                v-divider\n                v-list-item\n                  v-list-item-content.body-2 Deselect\n                  v-list-item-action #[kbd Esc]\n</template>\n\n<script>\nexport default {\n  computed: {\n    ctrlKey() { return /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl' },\n    altKey() { return /Mac/.test(navigator.platform) ? 'Option' : 'Alt' }\n  }\n}\n</script>\n\n<style lang='scss'>\n.editor-markdown-help {\n  position: fixed !important;\n  top: 112px;\n  left: 64px;\n  z-index: 10;\n  width: calc(100vw - 64px - 17px);\n  height: calc(100vh - 112px - 24px);\n  background-color: rgba(darken(mc('grey', '900'), 3%), .9) !important;\n  overflow: auto;\n\n  &-source {\n    background-color: mc('blue-grey', '900') !important;\n    border-radius: 7px;\n    font-family: 'Roboto Mono', monospace;\n    font-size: 14px;\n    color: #FFF !important;\n\n    .v-card__text {\n      color: #FFF !important;\n    }\n  }\n\n  &-result {\n    background-color: mc('blue-grey', '50') !important;\n    border-radius: 7px;\n    font-size: 14px;\n\n    code {\n      display: inline-block;\n      background-color: mc('pink', '50');\n      box-shadow: none;\n      font-size: inherit;\n    }\n\n    .contents {\n      padding-bottom: 16px;\n    }\n\n    .prismjs {\n      margin: 0;\n    }\n  }\n\n  &-kbd {\n    .v-list-item__action {\n      flex-direction: row;\n      align-items: center;\n\n      kbd {\n        display: inline-block;\n        border: 1px solid #ccc;\n        border-radius: 4px;\n        padding: 0.1em 0.5em;\n        margin: 0 0.2em;\n        box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #fff inset;\n        background-color: #f7f7f7;\n        color: mc('grey', '700');\n        font-size: 12px;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/editor/markdown/plantuml.js",
    "content": "const pako = require('pako')\n\n// ------------------------------------\n// Markdown - PlantUML Preprocessor\n// ------------------------------------\n\nmodule.exports = {\n  init (mdinst, conf) {\n    mdinst.use((md, opts) => {\n      const openMarker = opts.openMarker || '```plantuml'\n      const openChar = openMarker.charCodeAt(0)\n      const closeMarker = opts.closeMarker || '```'\n      const closeChar = closeMarker.charCodeAt(0)\n      const imageFormat = opts.imageFormat || 'svg'\n      const server = opts.server || 'https://plantuml.requarks.io'\n\n      md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {\n        let nextLine\n        let markup\n        let params\n        let token\n        let i\n        let autoClosed = false\n        let start = state.bMarks[startLine] + state.tShift[startLine]\n        let max = state.eMarks[startLine]\n\n        // Check out the first character quickly,\n        // this should filter out most of non-uml blocks\n        //\n        if (openChar !== state.src.charCodeAt(start)) { return false }\n\n        // Check out the rest of the marker string\n        //\n        for (i = 0; i < openMarker.length; ++i) {\n          if (openMarker[i] !== state.src[start + i]) { return false }\n        }\n\n        markup = state.src.slice(start, start + i)\n        params = state.src.slice(start + i, max)\n\n        // Since start is found, we can report success here in validation mode\n        //\n        if (silent) { return true }\n\n        // Search for the end of the block\n        //\n        nextLine = startLine\n\n        for (;;) {\n          nextLine++\n          if (nextLine >= endLine) {\n            // unclosed block should be autoclosed by end of document.\n            // also block seems to be autoclosed by end of parent\n            break\n          }\n\n          start = state.bMarks[nextLine] + state.tShift[nextLine]\n          max = state.eMarks[nextLine]\n\n          if (start < max && state.sCount[nextLine] < state.blkIndent) {\n            // non-empty line with negative indent should stop the list:\n            // - ```\n            //  test\n            break\n          }\n\n          if (closeChar !== state.src.charCodeAt(start)) {\n            // didn't find the closing fence\n            continue\n          }\n\n          if (state.sCount[nextLine] > state.sCount[startLine]) {\n            // closing fence should not be indented with respect of opening fence\n            continue\n          }\n\n          var closeMarkerMatched = true\n          for (i = 0; i < closeMarker.length; ++i) {\n            if (closeMarker[i] !== state.src[start + i]) {\n              closeMarkerMatched = false\n              break\n            }\n          }\n\n          if (!closeMarkerMatched) {\n            continue\n          }\n\n          // make sure tail has spaces only\n          if (state.skipSpaces(start + i) < max) {\n            continue\n          }\n\n          // found!\n          autoClosed = true\n          break\n        }\n\n        const contents = state.src\n          .split('\\n')\n          .slice(startLine + 1, nextLine)\n          .join('\\n')\n\n        // We generate a token list for the alt property, to mimic what the image parser does.\n        let altToken = []\n        // Remove leading space if any.\n        let alt = params ? params.slice(1) : 'uml diagram'\n        state.md.inline.parse(\n          alt,\n          state.md,\n          state.env,\n          altToken\n        )\n\n        var zippedCode = encode64(pako.deflate('@startuml\\n' + contents + '\\n@enduml', { to: 'string' }))\n\n        token = state.push('uml_diagram', 'img', 0)\n        // alt is constructed from children. No point in populating it here.\n        token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram'] ]\n        token.block = true\n        token.children = altToken\n        token.info = params\n        token.map = [ startLine, nextLine ]\n        token.markup = markup\n\n        state.line = nextLine + (autoClosed ? 1 : 0)\n\n        return true\n      }, {\n        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]\n      })\n      md.renderer.rules.uml_diagram = md.renderer.rules.image\n    }, {\n      openMarker: conf.openMarker,\n      closeMarker: conf.closeMarker,\n      imageFormat: conf.imageFormat,\n      server: conf.server\n    })\n  }\n}\n\nfunction encode64 (data) {\n  let r = ''\n  for (let i = 0; i < data.length; i += 3) {\n    if (i + 2 === data.length) {\n      r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)\n    } else if (i + 1 === data.length) {\n      r += append3bytes(data.charCodeAt(i), 0, 0)\n    } else {\n      r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))\n    }\n  }\n  return r\n}\n\nfunction append3bytes (b1, b2, b3) {\n  let c1 = b1 >> 2\n  let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)\n  let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)\n  let c4 = b3 & 0x3F\n  let r = ''\n  r += encode6bit(c1 & 0x3F)\n  r += encode6bit(c2 & 0x3F)\n  r += encode6bit(c3 & 0x3F)\n  r += encode6bit(c4 & 0x3F)\n  return r\n}\n\nfunction encode6bit(raw) {\n  let b = raw\n  if (b < 10) {\n    return String.fromCharCode(48 + b)\n  }\n  b -= 10\n  if (b < 26) {\n    return String.fromCharCode(65 + b)\n  }\n  b -= 26\n  if (b < 26) {\n    return String.fromCharCode(97 + b)\n  }\n  b -= 26\n  if (b === 0) {\n    return '-'\n  }\n  if (b === 1) {\n    return '_'\n  }\n  return '?'\n}\n"
  },
  {
    "path": "client/components/editor/markdown/tabset.js",
    "content": "import cash from 'cash-dom'\nimport _ from 'lodash'\n\nexport default {\n  format () {\n    for (let i = 1; i < 6; i++) {\n      cash(`.editor-markdown-preview-content h${i}.tabset`).each((idx, elm) => {\n        elm.innerHTML = 'Tabset ( rendered upon saving )'\n        cash(elm).nextUntil(_.times(i, t => `h${t + 1}`).join(', '), `h${i + 1}`).each((hidx, hd) => {\n          hd.classList.add('tabset-header')\n          cash(hd).nextUntil(_.times(i + 1, t => `h${t + 1}`).join(', ')).wrapAll('<div class=\"tabset-content\"></div>')\n        })\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "client/components/editor.vue",
    "content": "<template lang=\"pug\">\n  v-app.editor(:dark='$vuetify.theme.dark')\n    nav-header(dense)\n      template(slot='mid')\n        v-text-field.editor-title-input(\n          dark\n          solo\n          flat\n          v-model='currentPageTitle'\n          hide-details\n          background-color='black'\n          dense\n          full-width\n        )\n      template(slot='actions')\n        v-btn.mr-3.animated.fadeIn(color='amber', outlined, small, v-if='isConflict', @click='openConflict')\n          .overline.amber--text.mr-3 Conflict\n          status-indicator(intermediary, pulse)\n        v-btn.animated.fadeInDown(\n          text\n          color='green'\n          @click.exact='save'\n          @click.ctrl.exact='saveAndClose'\n          :class='{ \"is-icon\": $vuetify.breakpoint.mdAndDown }'\n          )\n          v-icon(color='green', :left='$vuetify.breakpoint.lgAndUp') mdi-check\n          span.grey--text(v-if='$vuetify.breakpoint.lgAndUp && mode !== `create` && !isDirty') {{ $t('editor:save.saved') }}\n          span.white--text(v-else-if='$vuetify.breakpoint.lgAndUp') {{ mode === 'create' ? $t('common:actions.create') : $t('common:actions.save') }}\n        v-btn.animated.fadeInDown.wait-p1s(\n          text\n          color='blue'\n          @click='openPropsModal'\n          :class='{ \"is-icon\": $vuetify.breakpoint.mdAndDown, \"mx-0\": !welcomeMode, \"ml-0\": welcomeMode }'\n          )\n          v-icon(color='blue', :left='$vuetify.breakpoint.lgAndUp') mdi-tag-text-outline\n          span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('common:actions.page') }}\n        v-btn.animated.fadeInDown.wait-p2s(\n          v-if='!welcomeMode'\n          text\n          color='red'\n          :class='{ \"is-icon\": $vuetify.breakpoint.mdAndDown }'\n          @click='exit'\n          )\n          v-icon(color='red', :left='$vuetify.breakpoint.lgAndUp') mdi-close\n          span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('common:actions.close') }}\n        v-divider.ml-3(vertical)\n    v-main\n      component(:is='currentEditor', :save='save')\n      editor-modal-properties(v-model='dialogProps')\n      editor-modal-editorselect(v-model='dialogEditorSelector')\n      editor-modal-unsaved(v-model='dialogUnsaved', @discard='exitGo')\n      component(:is='activeModal')\n\n    loader(v-model='dialogProgress', :title='$t(`editor:save.processing`)', :subtitle='$t(`editor:save.pleaseWait`)')\n    notify\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { get, sync } from 'vuex-pathify'\nimport { AtomSpinner } from 'epic-spinners'\nimport { Base64 } from 'js-base64'\nimport { StatusIndicator } from 'vue-status-indicator'\n\nimport editorStore from '../store/editor'\n\n/* global WIKI */\n\nWIKI.$store.registerModule('editor', editorStore)\n\nexport default {\n  i18nOptions: { namespaces: 'editor' },\n  components: {\n    AtomSpinner,\n    StatusIndicator,\n    editorApi: () => import(/* webpackChunkName: \"editor-api\", webpackMode: \"lazy\" */ './editor/editor-api.vue'),\n    editorCode: () => import(/* webpackChunkName: \"editor-code\", webpackMode: \"lazy\" */ './editor/editor-code.vue'),\n    editorCkeditor: () => import(/* webpackChunkName: \"editor-ckeditor\", webpackMode: \"lazy\" */ './editor/editor-ckeditor.vue'),\n    editorAsciidoc: () => import(/* webpackChunkName: \"editor-asciidoc\", webpackMode: \"lazy\" */ './editor/editor-asciidoc.vue'),\n    editorMarkdown: () => import(/* webpackChunkName: \"editor-markdown\", webpackMode: \"lazy\" */ './editor/editor-markdown.vue'),\n    editorRedirect: () => import(/* webpackChunkName: \"editor-redirect\", webpackMode: \"lazy\" */ './editor/editor-redirect.vue'),\n    editorModalEditorselect: () => import(/* webpackChunkName: \"editor\", webpackMode: \"eager\" */ './editor/editor-modal-editorselect.vue'),\n    editorModalProperties: () => import(/* webpackChunkName: \"editor\", webpackMode: \"eager\" */ './editor/editor-modal-properties.vue'),\n    editorModalUnsaved: () => import(/* webpackChunkName: \"editor\", webpackMode: \"eager\" */ './editor/editor-modal-unsaved.vue'),\n    editorModalMedia: () => import(/* webpackChunkName: \"editor\", webpackMode: \"eager\" */ './editor/editor-modal-media.vue'),\n    editorModalBlocks: () => import(/* webpackChunkName: \"editor\", webpackMode: \"eager\" */ './editor/editor-modal-blocks.vue'),\n    editorModalConflict: () => import(/* webpackChunkName: \"editor-conflict\", webpackMode: \"lazy\" */ './editor/editor-modal-conflict.vue'),\n    editorModalDrawio: () => import(/* webpackChunkName: \"editor\", webpackMode: \"eager\" */ './editor/editor-modal-drawio.vue')\n  },\n  props: {\n    locale: {\n      type: String,\n      default: 'en'\n    },\n    path: {\n      type: String,\n      default: 'home'\n    },\n    title: {\n      type: String,\n      default: 'Untitled Page'\n    },\n    description: {\n      type: String,\n      default: ''\n    },\n    tags: {\n      type: Array,\n      default: () => ([])\n    },\n    isPublished: {\n      type: Boolean,\n      default: true\n    },\n    scriptCss: {\n      type: String,\n      default: ''\n    },\n    publishStartDate: {\n      type: String,\n      default: ''\n    },\n    publishEndDate: {\n      type: String,\n      default: ''\n    },\n    scriptJs: {\n      type: String,\n      default: ''\n    },\n    initEditor: {\n      type: String,\n      default: null\n    },\n    initMode: {\n      type: String,\n      default: 'create'\n    },\n    initContent: {\n      type: String,\n      default: null\n    },\n    pageId: {\n      type: Number,\n      default: 0\n    },\n    checkoutDate: {\n      type: String,\n      default: new Date().toISOString()\n    },\n    effectivePermissions: {\n      type: String,\n      default: ''\n    }\n  },\n  data() {\n    return {\n      isSaving: false,\n      isConflict: false,\n      dialogProps: false,\n      dialogProgress: false,\n      dialogEditorSelector: false,\n      dialogUnsaved: false,\n      exitConfirmed: false,\n      initContentParsed: '',\n      savedState: {\n        description: '',\n        isPublished: false,\n        publishEndDate: '',\n        publishStartDate: '',\n        tags: '',\n        title: '',\n        css: '',\n        js: ''\n      }\n    }\n  },\n  computed: {\n    currentEditor: sync('editor/editor'),\n    activeModal: sync('editor/activeModal'),\n    mode: get('editor/mode'),\n    welcomeMode() { return this.mode === `create` && this.path === `home` },\n    currentPageTitle: sync('page/title'),\n    checkoutDateActive: sync('editor/checkoutDateActive'),\n    currentStyling: get('page/scriptCss'),\n    isDirty () {\n      return _.some([\n        this.initContentParsed !== this.$store.get('editor/content'),\n        this.locale !== this.$store.get('page/locale'),\n        this.path !== this.$store.get('page/path'),\n        this.savedState.title !== this.$store.get('page/title'),\n        this.savedState.description !== this.$store.get('page/description'),\n        this.savedState.tags !== this.$store.get('page/tags'),\n        this.savedState.isPublished !== this.$store.get('page/isPublished'),\n        this.savedState.publishStartDate !== this.$store.get('page/publishStartDate'),\n        this.savedState.publishEndDate !== this.$store.get('page/publishEndDate'),\n        this.savedState.css !== this.$store.get('page/scriptCss'),\n        this.savedState.js !== this.$store.get('page/scriptJs')\n      ], Boolean)\n    }\n  },\n  watch: {\n    currentEditor(newValue, oldValue) {\n      if (newValue !== '' && this.mode === 'create') {\n        _.delay(() => {\n          this.dialogProps = true\n        }, 500)\n      }\n    },\n    currentStyling(newValue) {\n      this.injectCustomCss(newValue)\n    }\n  },\n  created() {\n    this.$store.set('page/id', this.pageId)\n    this.$store.set('page/description', this.description)\n    this.$store.set('page/isPublished', this.isPublished)\n    this.$store.set('page/publishStartDate', this.publishStartDate)\n    this.$store.set('page/publishEndDate', this.publishEndDate)\n    this.$store.set('page/locale', this.locale)\n    this.$store.set('page/path', this.path)\n    this.$store.set('page/tags', this.tags)\n    this.$store.set('page/title', this.title)\n    this.$store.set('page/scriptCss', this.scriptCss)\n    this.$store.set('page/scriptJs', this.scriptJs)\n\n    this.$store.set('page/mode', 'edit')\n\n    this.setCurrentSavedState()\n\n    this.checkoutDateActive = this.checkoutDate\n\n    if (this.effectivePermissions) {\n      this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))\n    }\n  },\n  mounted() {\n    this.$store.set('editor/mode', this.initMode || 'create')\n\n    this.initContentParsed = this.initContent ? Base64.decode(this.initContent) : ''\n    this.$store.set('editor/content', this.initContentParsed)\n    if (this.mode === 'create' && !this.initEditor) {\n      _.delay(() => {\n        this.dialogEditorSelector = true\n      }, 500)\n    } else {\n      this.currentEditor = `editor${_.startCase(this.initEditor || 'markdown')}`\n    }\n\n    window.onbeforeunload = () => {\n      if (!this.exitConfirmed && this.initContentParsed !== this.$store.get('editor/content')) {\n        return this.$t('editor:unsavedWarning')\n      } else {\n        return undefined\n      }\n    }\n\n    this.$root.$on('resetEditorConflict', () => {\n      this.isConflict = false\n    })\n\n    // this.$store.set('editor/mode', 'edit')\n    // this.currentEditor = `editorApi`\n  },\n  methods: {\n    openPropsModal(name) {\n      this.dialogProps = true\n    },\n    showProgressDialog(textKey) {\n      this.dialogProgress = true\n    },\n    hideProgressDialog() {\n      this.dialogProgress = false\n    },\n    openConflict() {\n      this.$root.$emit('saveConflict')\n    },\n    async save({ rethrow = false, overwrite = false } = {}) {\n      this.showProgressDialog('saving')\n      this.isSaving = true\n\n      const saveTimeoutHandle = setTimeout(() => {\n        throw new Error('Save operation timed out.')\n      }, 30000)\n\n      try {\n        if (this.$store.get('editor/mode') === 'create') {\n          // --------------------------------------------\n          // -> CREATE PAGE\n          // --------------------------------------------\n\n          let resp = await this.$apollo.mutate({\n            mutation: gql`\n              mutation (\n                $content: String!\n                $description: String!\n                $editor: String!\n                $isPrivate: Boolean!\n                $isPublished: Boolean!\n                $locale: String!\n                $path: String!\n                $publishEndDate: Date\n                $publishStartDate: Date\n                $scriptCss: String\n                $scriptJs: String\n                $tags: [String]!\n                $title: String!\n              ) {\n                pages {\n                  create(\n                    content: $content\n                    description: $description\n                    editor: $editor\n                    isPrivate: $isPrivate\n                    isPublished: $isPublished\n                    locale: $locale\n                    path: $path\n                    publishEndDate: $publishEndDate\n                    publishStartDate: $publishStartDate\n                    scriptCss: $scriptCss\n                    scriptJs: $scriptJs\n                    tags: $tags\n                    title: $title\n                  ) {\n                    responseResult {\n                      succeeded\n                      errorCode\n                      slug\n                      message\n                    }\n                    page {\n                      id\n                      updatedAt\n                    }\n                  }\n                }\n              }\n            `,\n            variables: {\n              content: this.$store.get('editor/content'),\n              description: this.$store.get('page/description'),\n              editor: this.$store.get('editor/editorKey'),\n              locale: this.$store.get('page/locale'),\n              isPrivate: false,\n              isPublished: this.$store.get('page/isPublished'),\n              path: this.$store.get('page/path'),\n              publishEndDate: this.$store.get('page/publishEndDate') || '',\n              publishStartDate: this.$store.get('page/publishStartDate') || '',\n              scriptCss: this.$store.get('page/scriptCss'),\n              scriptJs: this.$store.get('page/scriptJs'),\n              tags: this.$store.get('page/tags'),\n              title: this.$store.get('page/title')\n            }\n          })\n          resp = _.get(resp, 'data.pages.create', {})\n          if (_.get(resp, 'responseResult.succeeded')) {\n            this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)\n            this.isConflict = false\n            this.$store.commit('showNotification', {\n              message: this.$t('editor:save.createSuccess'),\n              style: 'success',\n              icon: 'check'\n            })\n            this.$store.set('editor/id', _.get(resp, 'page.id'))\n            this.$store.set('editor/mode', 'update')\n            this.exitConfirmed = true\n            window.location.assign(`/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)\n          } else {\n            throw new Error(_.get(resp, 'responseResult.message'))\n          }\n        } else {\n          // --------------------------------------------\n          // -> UPDATE EXISTING PAGE\n          // --------------------------------------------\n\n          const conflictResp = await this.$apollo.query({\n            query: gql`\n              query ($id: Int!, $checkoutDate: Date!) {\n                pages {\n                  checkConflicts(id: $id, checkoutDate: $checkoutDate)\n                }\n              }\n            `,\n            fetchPolicy: 'network-only',\n            variables: {\n              id: this.pageId,\n              checkoutDate: this.checkoutDateActive\n            }\n          })\n          if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {\n            this.$root.$emit('saveConflict')\n            throw new Error(this.$t('editor:conflict.warning'))\n          }\n\n          let resp = await this.$apollo.mutate({\n            mutation: gql`\n              mutation (\n                $id: Int!\n                $content: String\n                $description: String\n                $editor: String\n                $isPrivate: Boolean\n                $isPublished: Boolean\n                $locale: String\n                $path: String\n                $publishEndDate: Date\n                $publishStartDate: Date\n                $scriptCss: String\n                $scriptJs: String\n                $tags: [String]\n                $title: String\n              ) {\n                pages {\n                  update(\n                    id: $id\n                    content: $content\n                    description: $description\n                    editor: $editor\n                    isPrivate: $isPrivate\n                    isPublished: $isPublished\n                    locale: $locale\n                    path: $path\n                    publishEndDate: $publishEndDate\n                    publishStartDate: $publishStartDate\n                    scriptCss: $scriptCss\n                    scriptJs: $scriptJs\n                    tags: $tags\n                    title: $title\n                  ) {\n                    responseResult {\n                      succeeded\n                      errorCode\n                      slug\n                      message\n                    }\n                    page {\n                      updatedAt\n                    }\n                  }\n                }\n              }\n            `,\n            variables: {\n              id: this.$store.get('page/id'),\n              content: this.$store.get('editor/content'),\n              description: this.$store.get('page/description'),\n              editor: this.$store.get('editor/editorKey'),\n              locale: this.$store.get('page/locale'),\n              isPrivate: false,\n              isPublished: this.$store.get('page/isPublished'),\n              path: this.$store.get('page/path'),\n              publishEndDate: this.$store.get('page/publishEndDate') || '',\n              publishStartDate: this.$store.get('page/publishStartDate') || '',\n              scriptCss: this.$store.get('page/scriptCss'),\n              scriptJs: this.$store.get('page/scriptJs'),\n              tags: this.$store.get('page/tags'),\n              title: this.$store.get('page/title')\n            }\n          })\n          resp = _.get(resp, 'data.pages.update', {})\n          if (_.get(resp, 'responseResult.succeeded')) {\n            this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)\n            this.isConflict = false\n            this.$store.commit('showNotification', {\n              message: this.$t('editor:save.updateSuccess'),\n              style: 'success',\n              icon: 'check'\n            })\n            if (this.locale !== this.$store.get('page/locale') || this.path !== this.$store.get('page/path')) {\n              _.delay(() => {\n                window.location.replace(`/e/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)\n              }, 1000)\n            }\n          } else {\n            throw new Error(_.get(resp, 'responseResult.message'))\n          }\n        }\n\n        this.initContentParsed = this.$store.get('editor/content')\n        this.setCurrentSavedState()\n      } catch (err) {\n        this.$store.commit('showNotification', {\n          message: err.message,\n          style: 'error',\n          icon: 'warning'\n        })\n        if (rethrow === true) {\n          clearTimeout(saveTimeoutHandle)\n          this.isSaving = false\n          this.hideProgressDialog()\n          throw err\n        }\n      }\n      clearTimeout(saveTimeoutHandle)\n      this.isSaving = false\n      this.hideProgressDialog()\n    },\n    async saveAndClose() {\n      try {\n        if (this.$store.get('editor/mode') === 'create') {\n          await this.save()\n        } else {\n          await this.save({ rethrow: true })\n          await this.exit()\n        }\n      } catch (err) {\n        // Error is already handled\n      }\n    },\n    async exit() {\n      if (this.isDirty) {\n        this.dialogUnsaved = true\n      } else {\n        this.exitGo()\n      }\n    },\n    exitGo() {\n      this.$store.commit(`loadingStart`, 'editor-close')\n      this.currentEditor = ''\n      this.exitConfirmed = true\n      _.delay(() => {\n        if (this.$store.get('editor/mode') === 'create') {\n          window.location.assign(`/`)\n        } else {\n          window.location.assign(`/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)\n        }\n      }, 500)\n    },\n    setCurrentSavedState () {\n      this.savedState = {\n        description: this.$store.get('page/description'),\n        isPublished: this.$store.get('page/isPublished'),\n        publishEndDate: this.$store.get('page/publishEndDate') || '',\n        publishStartDate: this.$store.get('page/publishStartDate') || '',\n        tags: this.$store.get('page/tags'),\n        title: this.$store.get('page/title'),\n        css: this.$store.get('page/scriptCss'),\n        js: this.$store.get('page/scriptJs')\n      }\n    },\n    injectCustomCss: _.debounce(css => {\n      const oldStyl = document.querySelector('#editor-script-css')\n      if (oldStyl) {\n        document.head.removeChild(oldStyl)\n      }\n      if (!_.isEmpty(css)) {\n        const styl = document.createElement('style')\n        styl.type = 'text/css'\n        styl.id = 'editor-script-css'\n        document.head.appendChild(styl)\n        styl.appendChild(document.createTextNode(css))\n      }\n    }, 1000)\n  },\n  apollo: {\n    isConflict: {\n      query: gql`\n        query ($id: Int!, $checkoutDate: Date!) {\n          pages {\n            checkConflicts(id: $id, checkoutDate: $checkoutDate)\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      pollInterval: 5000,\n      variables () {\n        return {\n          id: this.pageId,\n          checkoutDate: this.checkoutDateActive\n        }\n      },\n      update: (data) => _.cloneDeep(data.pages.checkConflicts),\n      skip () {\n        return this.mode === 'create' || this.isSaving || !this.isDirty\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n  .editor {\n    background-color: mc('grey', '900') !important;\n    min-height: 100vh;\n\n    .application--wrap {\n      background-color: mc('grey', '900');\n    }\n\n    &-title-input input {\n      text-align: center;\n    }\n  }\n\n  .atom-spinner.is-inline {\n    display: inline-block;\n  }\n\n</style>\n"
  },
  {
    "path": "client/components/history.vue",
    "content": "<template lang='pug'>\n  v-app(:dark='$vuetify.theme.dark').history\n    nav-header\n    v-content\n      v-toolbar(color='primary', dark)\n        .subheading Viewing history of #[strong /{{path}}]\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          .caption.blue--text.text--lighten-3.mr-4 Trail Length: {{total}}\n          .caption.blue--text.text--lighten-3 ID: {{pageId}}\n          v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Live Version\n      v-container(fluid, grid-list-xl)\n        v-layout(row, wrap)\n          v-flex(xs12, md4)\n            v-chip.my-0.ml-6(\n              label\n              small\n              :color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`'\n              :class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-2`'\n              )\n              span Live\n            v-timeline(\n              dense\n              )\n              v-timeline-item.pb-2(\n                v-for='(ph, idx) in fullTrail'\n                :key='ph.versionId'\n                :small='ph.actionType === `edit`'\n                :color='trailColor(ph.actionType)'\n                :icon='trailIcon(ph.actionType)'\n                )\n                v-card.radius-7(flat, :class='trailBgColor(ph.actionType)')\n                  v-toolbar(flat, :color='trailBgColor(ph.actionType)', height='40')\n                    .caption(:title='$options.filters.moment(ph.versionDate, `LLL`)') {{ ph.versionDate | moment('ll') }}\n                    v-divider.mx-3(vertical)\n                    .caption(v-if='ph.actionType === `edit`') Edited by #[strong {{ ph.authorName }}]\n                    .caption(v-else-if='ph.actionType === `move`') Moved from #[strong {{ph.valueBefore}}] to #[strong {{ph.valueAfter}}] by #[strong {{ ph.authorName }}]\n                    .caption(v-else-if='ph.actionType === `initial`') Created by #[strong {{ ph.authorName }}]\n                    .caption(v-else-if='ph.actionType === `live`') Last Edited by #[strong {{ ph.authorName }}]\n                    .caption(v-else) Unknown Action by #[strong {{ ph.authorName }}]\n                    v-spacer\n                    v-menu(offset-x, left)\n                      template(v-slot:activator='{ on }')\n                        v-btn.mr-2.radius-4(icon, v-on='on', small, tile): v-icon mdi-dots-horizontal\n                      v-list(dense, nav).history-promptmenu\n                        v-list-item(@click='setDiffSource(ph.versionId)', :disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0')\n                          v-list-item-avatar(size='24'): v-avatar A\n                          v-list-item-title Set as Differencing Source\n                        v-list-item(@click='setDiffTarget(ph.versionId)', :disabled='ph.versionId <= diffSource && ph.versionId !== 0')\n                          v-list-item-avatar(size='24'): v-avatar B\n                          v-list-item-title Set as Differencing Target\n                        v-list-item(@click='viewSource(ph.versionId)')\n                          v-list-item-avatar(size='24'): v-icon mdi-code-tags\n                          v-list-item-title View Source\n                        v-list-item(@click='download(ph.versionId)')\n                          v-list-item-avatar(size='24'): v-icon mdi-cloud-download-outline\n                          v-list-item-title Download Version\n                        v-list-item(@click='restore(ph.versionId, ph.versionDate)', :disabled='ph.versionId === 0')\n                          v-list-item-avatar(size='24'): v-icon(:disabled='ph.versionId === 0') mdi-history\n                          v-list-item-title Restore\n                        v-list-item(@click='branchOff(ph.versionId)')\n                          v-list-item-avatar(size='24'): v-icon mdi-source-branch\n                          v-list-item-title Branch off from here\n                    v-btn.mr-2.radius-4(\n                      @click='setDiffSource(ph.versionId)'\n                      icon\n                      small\n                      depressed\n                      tile\n                      :class='diffSource === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'\n                      :disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0'\n                      ): strong A\n                    v-btn.mr-0.radius-4(\n                      @click='setDiffTarget(ph.versionId)'\n                      icon\n                      small\n                      depressed\n                      tile\n                      :class='diffTarget === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'\n                      :disabled='ph.versionId <= diffSource && ph.versionId !== 0'\n                      ): strong B\n\n            v-btn.ma-0.radius-7(\n              v-if='total > trail.length'\n              block\n              color='primary'\n              @click='loadMore'\n              )\n              .caption.white--text Load More...\n\n            v-chip.ma-0(\n              v-else\n              label\n              small\n              :color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`'\n              :class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-2`'\n              ) End of history trail\n\n          v-flex(xs12, md8)\n            v-card.radius-7(:class='$vuetify.breakpoint.mdAndUp ? `mt-8` : ``')\n              v-card-text\n                v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-2` : `lighten-4`')\n                  v-row(no-gutters, align='center')\n                    v-col\n                      v-card-text\n                        .subheading {{target.title}}\n                        .caption {{target.description}}\n                    v-col.text-right.py-3(cols='2', v-if='$vuetify.breakpoint.mdAndUp')\n                      v-btn.mr-3(:color='$vuetify.theme.dark ? `white` : `grey darken-3`', small, dark, outlined, @click='toggleViewMode')\n                        v-icon(left) mdi-eye\n                        .overline View Mode\n                v-card.mt-3(light, v-html='diffHTML', flat)\n\n    v-dialog(v-model='isRestoreConfirmDialogShown', max-width='650', persistent)\n      v-card\n        .dialog-header.is-orange {{$t('history:restore.confirmTitle')}}\n        v-card-text.pa-4\n          i18next(tag='span', path='history:restore.confirmText')\n            strong(place='date') {{ restoreTarget.versionDate | moment('LLL') }}\n        v-card-actions\n          v-spacer\n          v-btn(text, @click='isRestoreConfirmDialogShown = false', :disabled='restoreLoading') {{$t('common:actions.cancel')}}\n          v-btn(color='orange darken-2', dark, @click='restoreConfirm', :loading='restoreLoading') {{$t('history:restore.confirmButton')}}\n\n    page-selector(mode='create', v-model='branchOffOpts.modal', :open-handler='branchOffHandle', :path='branchOffOpts.path', :locale='branchOffOpts.locale')\n\n    nav-footer\n    notify\n    search-results\n</template>\n\n<script>\nimport * as Diff2Html from 'diff2html'\nimport { createPatch } from 'diff'\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\n\nexport default {\n  i18nOptions: { namespaces: 'history' },\n  props: {\n    pageId: {\n      type: Number,\n      default: 0\n    },\n    locale: {\n      type: String,\n      default: 'en'\n    },\n    path: {\n      type: String,\n      default: 'home'\n    },\n    title: {\n      type: String,\n      default: 'Untitled Page'\n    },\n    description: {\n      type: String,\n      default: ''\n    },\n    createdAt: {\n      type: String,\n      default: ''\n    },\n    updatedAt: {\n      type: String,\n      default: ''\n    },\n    tags: {\n      type: Array,\n      default: () => ([])\n    },\n    authorName: {\n      type: String,\n      default: 'Unknown'\n    },\n    authorId: {\n      type: Number,\n      default: 0\n    },\n    isPublished: {\n      type: Boolean,\n      default: false\n    },\n    liveContent: {\n      type: String,\n      default: ''\n    },\n    effectivePermissions: {\n      type: String,\n      default: ''\n    }\n  },\n  data () {\n    return {\n      source: {\n        versionId: 0,\n        content: '',\n        title: '',\n        description: ''\n      },\n      target: {\n        versionId: 0,\n        content: '',\n        title: '',\n        description: ''\n      },\n      trail: [],\n      diffSource: 0,\n      diffTarget: 0,\n      offsetPage: 0,\n      total: 0,\n      viewMode: 'line-by-line',\n      cache: [],\n      restoreTarget: {\n        versionId: 0,\n        versionDate: ''\n      },\n      branchOffOpts: {\n        versionId: 0,\n        locale: 'en',\n        path: 'new-page',\n        modal: false\n      },\n      isRestoreConfirmDialogShown: false,\n      restoreLoading: false\n    }\n  },\n  computed: {\n    fullTrail () {\n      const liveTrailItem = {\n        versionId: 0,\n        authorId: this.authorId,\n        authorName: this.authorName,\n        actionType: 'live',\n        valueBefore: null,\n        valueAfter: null,\n        versionDate: this.updatedAt\n      }\n      // -> Check for move between latest and live\n      const prevPage = _.find(this.cache, ['versionId', _.get(this.trail, '[0].versionId', -1)])\n      if (prevPage && this.path !== prevPage.path) {\n        liveTrailItem.actionType = 'move'\n        liveTrailItem.valueBefore = prevPage.path\n        liveTrailItem.valueAfter = this.path\n      }\n      // -> Combine trail with live\n      return [\n        liveTrailItem,\n        ...this.trail\n      ]\n    },\n    diffs () {\n      return createPatch(`/${this.path}`, this.source.content, this.target.content)\n    },\n    diffHTML () {\n      return Diff2Html.html(this.diffs, {\n        inputFormat: 'diff',\n        drawFileList: false,\n        matching: 'lines',\n        outputFormat: this.viewMode\n      })\n    }\n  },\n  watch: {\n    trail (newValue, oldValue) {\n      if (newValue && newValue.length > 0) {\n        this.diffTarget = 0\n        this.diffSource = _.get(_.head(newValue), 'versionId', 0)\n      }\n    },\n    async diffSource (newValue, oldValue) {\n      if (this.diffSource !== this.source.versionId) {\n        const page = _.find(this.cache, { versionId: newValue })\n        if (page) {\n          this.source = page\n        } else {\n          this.source = await this.loadVersion(newValue)\n        }\n      }\n    },\n    async diffTarget (newValue, oldValue) {\n      if (this.diffTarget !== this.target.versionId) {\n        const page = _.find(this.cache, { versionId: newValue })\n        if (page) {\n          this.target = page\n        } else {\n          this.target = await this.loadVersion(newValue)\n        }\n      }\n    }\n  },\n  created () {\n    this.$store.commit('page/SET_ID', this.id)\n    this.$store.commit('page/SET_LOCALE', this.locale)\n    this.$store.commit('page/SET_PATH', this.path)\n\n    this.$store.commit('page/SET_MODE', 'history')\n\n    this.cache.push({\n      action: 'live',\n      authorId: this.authorId,\n      authorName: this.authorName,\n      content: this.liveContent,\n      contentType: '',\n      createdAt: this.createdAt,\n      description: this.description,\n      editor: '',\n      isPrivate: false,\n      isPublished: this.isPublished,\n      locale: this.locale,\n      pageId: this.pageId,\n      path: this.path,\n      publishEndDate: '',\n      publishStartDate: '',\n      tags: this.tags,\n      title: this.title,\n      versionId: 0,\n      versionDate: this.updatedAt\n    })\n\n    this.target = this.cache[0]\n\n    if (this.effectivePermissions) {\n      this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))\n    }\n  },\n  methods: {\n    async loadVersion (versionId) {\n      this.$store.commit(`loadingStart`, 'history-version-' + versionId)\n      const resp = await this.$apollo.query({\n        query: gql`\n          query ($pageId: Int!, $versionId: Int!) {\n            pages {\n              version (pageId: $pageId, versionId: $versionId) {\n                action\n                authorId\n                authorName\n                content\n                contentType\n                createdAt\n                versionDate\n                description\n                editor\n                isPrivate\n                isPublished\n                locale\n                pageId\n                path\n                publishEndDate\n                publishStartDate\n                tags\n                title\n                versionId\n              }\n            }\n          }\n        `,\n        variables: {\n          versionId,\n          pageId: this.pageId\n        }\n      })\n      this.$store.commit(`loadingStop`, 'history-version-' + versionId)\n      const page = _.get(resp, 'data.pages.version', null)\n      if (page) {\n        this.cache.push(page)\n        return page\n      } else {\n        return { content: '' }\n      }\n    },\n    viewSource (versionId) {\n      window.location.assign(`/s/${this.locale}/${this.path}?v=${versionId}`)\n    },\n    download (versionId) {\n      window.location.assign(`/d/${this.locale}/${this.path}?v=${versionId}`)\n    },\n    restore (versionId, versionDate) {\n      this.restoreTarget = {\n        versionId,\n        versionDate\n      }\n      this.isRestoreConfirmDialogShown = true\n    },\n    async restoreConfirm () {\n      this.restoreLoading = true\n      this.$store.commit(`loadingStart`, 'history-restore')\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($pageId: Int!, $versionId: Int!) {\n              pages {\n                restore (pageId: $pageId, versionId: $versionId) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            versionId: this.restoreTarget.versionId,\n            pageId: this.pageId\n          }\n        })\n        if (_.get(resp, 'data.pages.restore.responseResult.succeeded', false) === true) {\n          this.$store.commit('showNotification', {\n            style: 'success',\n            message: this.$t('history:restore.success'),\n            icon: 'check'\n          })\n          this.isRestoreConfirmDialogShown = false\n          setTimeout(() => {\n            window.location.assign(`/${this.locale}/${this.path}`)\n          }, 1000)\n        } else {\n          throw new Error(_.get(resp, 'data.pages.restore.responseResult.message', 'An unexpected error occurred'))\n        }\n      } catch (err) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n      }\n      this.$store.commit(`loadingStop`, 'history-restore')\n      this.restoreLoading = false\n    },\n    branchOff (versionId) {\n      const pathParts = this.path.split('/')\n      this.branchOffOpts = {\n        versionId: versionId,\n        locale: this.locale,\n        path: (pathParts.length > 1) ? _.initial(pathParts).join('/') + `/new-page` : `new-page`,\n        modal: true\n      }\n    },\n    branchOffHandle ({ locale, path }) {\n      window.location.assign(`/e/${locale}/${path}?from=${this.pageId},${this.branchOffOpts.versionId}`)\n    },\n    toggleViewMode () {\n      this.viewMode = (this.viewMode === 'line-by-line') ? 'side-by-side' : 'line-by-line'\n    },\n    goLive () {\n      window.location.assign(`/${this.path}`)\n    },\n    setDiffSource (versionId) {\n      this.diffSource = versionId\n    },\n    setDiffTarget (versionId) {\n      this.diffTarget = versionId\n    },\n    loadMore () {\n      this.offsetPage++\n      this.$apollo.queries.trail.fetchMore({\n        variables: {\n          id: this.pageId,\n          offsetPage: this.offsetPage,\n          offsetSize: this.$vuetify.breakpoint.mdAndUp ? 25 : 5\n        },\n        updateQuery: (previousResult, { fetchMoreResult }) => {\n          return {\n            pages: {\n              history: {\n                total: previousResult.pages.history.total,\n                trail: [...previousResult.pages.history.trail, ...fetchMoreResult.pages.history.trail],\n                __typename: previousResult.pages.history.__typename\n              },\n              __typename: previousResult.pages.__typename\n            }\n          }\n        }\n      })\n    },\n    trailColor (actionType) {\n      switch (actionType) {\n        case 'edit':\n          return 'primary'\n        case 'move':\n          return 'purple'\n        case 'initial':\n          return 'teal'\n        case 'live':\n          return 'orange'\n        default:\n          return 'grey'\n      }\n    },\n    trailIcon (actionType) {\n      switch (actionType) {\n        case 'edit':\n          return '' // 'mdi-pencil'\n        case 'move':\n          return 'mdi-forward'\n        case 'initial':\n          return 'mdi-plus'\n        case 'live':\n          return 'mdi-atom-variant'\n        default:\n          return 'mdi-alert'\n      }\n    },\n    trailBgColor (actionType) {\n      switch (actionType) {\n        case 'move':\n          return this.$vuetify.theme.dark ? 'purple' : 'purple lighten-5'\n        case 'initial':\n          return this.$vuetify.theme.dark ? 'teal darken-3' : 'teal lighten-5'\n        case 'live':\n          return this.$vuetify.theme.dark ? 'orange darken-3' : 'orange lighten-5'\n        default:\n          return this.$vuetify.theme.dark ? 'grey darken-3' : 'grey lighten-4'\n      }\n    }\n  },\n  apollo: {\n    trail: {\n      query: gql`\n        query($id: Int!, $offsetPage: Int, $offsetSize: Int) {\n          pages {\n            history(id:$id, offsetPage:$offsetPage, offsetSize:$offsetSize) {\n              trail {\n                versionId\n                authorId\n                authorName\n                actionType\n                valueBefore\n                valueAfter\n                versionDate\n              }\n              total\n            }\n          }\n        }\n      `,\n      variables () {\n        return {\n          id: this.pageId,\n          offsetPage: 0,\n          offsetSize: this.$vuetify.breakpoint.mdAndUp ? 25 : 5\n        }\n      },\n      manual: true,\n      result ({ data, loading, networkStatus }) {\n        this.total = data.pages.history.total\n        this.trail = data.pages.history.trail\n      },\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'history-trail-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.history {\n  &-promptmenu {\n    border-top: 5px solid mc('blue', '700');\n  }\n\n  .d2h-file-wrapper {\n    border: 1px solid #EEE;\n    border-left: none;\n  }\n\n  .d2h-file-header {\n    display: none;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/login.vue",
    "content": "<template lang=\"pug\">\n  v-app\n    .login(:style='`background-image: url(` + bgUrl + `);`')\n      .login-sd\n        .d-flex.mb-5\n          .login-logo\n            v-avatar(tile, size='34')\n              v-img(:src='logoUrl')\n          .login-title\n            .text-h6.grey--text.text--darken-4 {{ siteTitle }}\n        v-alert.mb-0(\n          v-model='errorShown'\n          transition='slide-y-reverse-transition'\n          color='red darken-2'\n          tile\n          dark\n          dense\n          icon='mdi-alert'\n          )\n          .body-2 {{errorMessage}}\n        //-------------------------------------------------\n        //- PROVIDERS LIST\n        //-------------------------------------------------\n        template(v-if='screen === `login` && strategies.length > 1')\n          .login-subtitle\n            .text-subtitle-1 {{$t('auth:selectAuthProvider')}}\n          .login-list\n            v-list.elevation-1.radius-7(nav, light)\n              v-list-item-group(v-model='selectedStrategyKey')\n                v-list-item(\n                  v-for='(stg, idx) of filteredStrategies'\n                  :key='stg.key'\n                  :value='stg.key'\n                  :color='stg.strategy.color'\n                  )\n                  v-avatar.mr-3(tile, size='24', v-html='stg.strategy.icon')\n                  span.text-none {{stg.displayName}}\n        //-------------------------------------------------\n        //- LOGIN FORM\n        //-------------------------------------------------\n        template(v-if='screen === `login` && selectedStrategy.strategy.useForm')\n          .login-subtitle\n            .text-subtitle-1 {{$t('auth:enterCredentials')}}\n          .login-form\n            v-text-field(\n              solo\n              flat\n              prepend-inner-icon='mdi-clipboard-account'\n              background-color='white'\n              color='blue darken-2'\n              hide-details\n              ref='iptEmail'\n              v-model='username'\n              :placeholder='isUsernameEmail ? $t(`auth:fields.email`) : $t(`auth:fields.username`)'\n              :type='isUsernameEmail ? `email` : `text`'\n              :autocomplete='isUsernameEmail ? `email` : `username`'\n              light\n              )\n            v-text-field.mt-2(\n              solo\n              flat\n              prepend-inner-icon='mdi-form-textbox-password'\n              background-color='white'\n              color='blue darken-2'\n              hide-details\n              ref='iptPassword'\n              v-model='password'\n              :append-icon='hidePassword ? \"mdi-eye-off\" : \"mdi-eye\"'\n              @click:append='() => (hidePassword = !hidePassword)'\n              :type='hidePassword ? \"password\" : \"text\"'\n              :placeholder='$t(\"auth:fields.password\")'\n              autocomplete='current-password'\n              @keyup.enter='login'\n              light\n            )\n            v-btn.mt-2.text-none(\n              width='100%'\n              large\n              color='blue darken-2'\n              dark\n              @click='login'\n              :loading='isLoading'\n              ) {{ $t('auth:actions.login') }}\n            .text-center.mt-5\n              v-btn.text-none(\n                text\n                rounded\n                color='grey darken-3'\n                @click.stop.prevent='forgotPassword'\n                href='#forgot'\n                ): .caption {{ $t('auth:forgotPasswordLink') }}\n              v-btn.text-none(\n                v-if='selectedStrategyKey === `local` && selectedStrategy.selfRegistration'\n                color='indigo darken-2'\n                text\n                rounded\n                href='/register'\n                ): .caption {{ $t('auth:switchToRegister.link') }}\n        //-------------------------------------------------\n        //- FORGOT PASSWORD FORM\n        //-------------------------------------------------\n        template(v-if='screen === `forgot`')\n          .login-subtitle\n            .text-subtitle-1 {{$t('auth:forgotPasswordTitle')}}\n          .login-info {{ $t('auth:forgotPasswordSubtitle') }}\n          .login-form\n            v-text-field(\n              solo\n              flat\n              prepend-inner-icon='mdi-clipboard-account'\n              background-color='white'\n              color='blue darken-2'\n              hide-details\n              ref='iptForgotPwdEmail'\n              v-model='username'\n              :placeholder='$t(`auth:fields.email`)'\n              type='email'\n              autocomplete='email'\n              light\n              )\n            v-btn.mt-2.text-none(\n              width='100%'\n              large\n              color='blue darken-2'\n              dark\n              @click='forgotPasswordSubmit'\n              :loading='isLoading'\n              ) {{ $t('auth:sendResetPassword') }}\n            .text-center.mt-5\n              v-btn.text-none(\n                text\n                rounded\n                color='grey darken-3'\n                @click.stop.prevent='screen = `login`'\n                href='#forgot'\n                ): .caption {{ $t('auth:forgotPasswordCancel') }}\n        //-------------------------------------------------\n        //- CHANGE PASSWORD FORM\n        //-------------------------------------------------\n        template(v-if='screen === `changePwd`')\n          .login-subtitle\n            .text-subtitle-1 {{ $t('auth:changePwd.subtitle') }}\n          .login-form\n            v-text-field.mt-2(\n              type='password'\n              solo\n              flat\n              prepend-inner-icon='mdi-form-textbox-password'\n              background-color='white'\n              color='blue darken-2'\n              hide-details\n              ref='iptNewPassword'\n              v-model='newPassword'\n              :placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'\n              autocomplete='new-password'\n              light\n              )\n              password-strength(slot='progress', v-model='newPassword')\n            v-text-field.mt-2(\n              type='password'\n              solo\n              flat\n              prepend-inner-icon='mdi-form-textbox-password'\n              background-color='white'\n              color='blue darken-2'\n              hide-details\n              v-model='newPasswordVerify'\n              :placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'\n              autocomplete='new-password'\n              @keyup.enter='changePassword'\n              light\n            )\n            v-btn.mt-2.text-none(\n              width='100%'\n              large\n              color='blue darken-2'\n              dark\n              @click='changePassword'\n              :loading='isLoading'\n              ) {{ $t('auth:changePwd.proceed') }}\n\n    //-------------------------------------------------\n    //- TFA FORM\n    //-------------------------------------------------\n    v-dialog(v-model='isTFAShown', max-width='500', persistent)\n      v-card\n        .login-tfa.text-center.pa-5.grey--text.text--darken-3\n          img(src='_assets/svg/icon-pin-pad.svg')\n          .subtitle-2 {{$t('auth:tfaFormTitle')}}\n          v-text-field.login-tfa-field.mt-2(\n            solo\n            flat\n            background-color='white'\n            color='blue darken-2'\n            hide-details\n            ref='iptTFA'\n            v-model='securityCode'\n            :placeholder='$t(\"auth:tfa.placeholder\")'\n            autocomplete='one-time-code'\n            @keyup.enter='verifySecurityCode(false)'\n            light\n          )\n          v-btn.mt-2.text-none(\n            width='100%'\n            large\n            color='blue darken-2'\n            dark\n            @click='verifySecurityCode(false)'\n            :loading='isLoading'\n            ) {{ $t('auth:tfa.verifyToken') }}\n\n    //-------------------------------------------------\n    //- SETUP TFA FORM\n    //-------------------------------------------------\n    v-dialog(v-model='isTFASetupShown', max-width='600', persistent)\n      v-card\n        .login-tfa.text-center.pa-5.grey--text.text--darken-3\n          .subtitle-1.primary--text {{$t('auth:tfaSetupTitle')}}\n          v-divider.my-5\n          .subtitle-2 {{$t('auth:tfaSetupInstrFirst')}}\n          .caption (#[a(href='https://authy.com/', target='_blank', noopener) Authy], #[a(href='https://support.google.com/accounts/answer/1066447', target='_blank', noopener) Google Authenticator], #[a(href='https://www.microsoft.com/en-us/account/authenticator', target='_blank', noopener) Microsoft Authenticator], etc.)\n          .login-tfa-qr.mt-5(v-if='isTFASetupShown', v-html='tfaQRImage')\n          .subtitle-2.mt-5 {{$t('auth:tfaSetupInstrSecond')}}\n          v-text-field.login-tfa-field.mt-2(\n            solo\n            flat\n            background-color='white'\n            color='blue darken-2'\n            hide-details\n            ref='iptTFASetup'\n            v-model='securityCode'\n            :placeholder='$t(\"auth:tfa.placeholder\")'\n            autocomplete='one-time-code'\n            @keyup.enter='verifySecurityCode(true)'\n            light\n          )\n          v-btn.mt-2.text-none(\n            width='100%'\n            large\n            color='blue darken-2'\n            dark\n            @click='verifySecurityCode(true)'\n            :loading='isLoading'\n            ) {{ $t('auth:tfa.verifyToken') }}\n\n    loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')\n    notify(style='padding-top: 64px;')\n</template>\n\n<script>\n/* global siteConfig */\n\n// <span>Photo by <a href=\"https://unsplash.com/@isaacquesada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText\">Isaac Quesada</a> on <a href=\"/t/textures-patterns?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText\">Unsplash</a></span>\n\nimport _ from 'lodash'\nimport Cookies from 'js-cookie'\nimport gql from 'graphql-tag'\nimport { sync } from 'vuex-pathify'\n\nexport default {\n  i18nOptions: { namespaces: 'auth' },\n  props: {\n    bgUrl: {\n      type: String,\n      default: ''\n    },\n    hideLocal: {\n      type: Boolean,\n      default: false\n    },\n    changePwdContinuationToken: {\n      type: String,\n      default: null\n    }\n  },\n  data () {\n    return {\n      error: false,\n      strategies: [],\n      selectedStrategyKey: 'unselected',\n      selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },\n      screen: 'login',\n      username: '',\n      password: '',\n      hidePassword: true,\n      securityCode: '',\n      continuationToken: '',\n      isLoading: false,\n      loaderColor: 'grey darken-4',\n      loaderTitle: 'Working...',\n      isShown: false,\n      newPassword: '',\n      newPasswordVerify: '',\n      isTFAShown: false,\n      isTFASetupShown: false,\n      tfaQRImage: '',\n      errorShown: false,\n      errorMessage: ''\n    }\n  },\n  computed: {\n    activeModal: sync('editor/activeModal'),\n    siteTitle () {\n      return siteConfig.title\n    },\n    isSocialShown () {\n      return this.strategies.length > 1\n    },\n    logoUrl () { return siteConfig.logoUrl },\n    filteredStrategies () {\n      const qParams = new URLSearchParams(window.location.search)\n      if (this.hideLocal && !qParams.has('all')) {\n        return _.reject(this.strategies, ['key', 'local'])\n      } else {\n        return this.strategies\n      }\n    },\n    isUsernameEmail () {\n      return this.selectedStrategy.strategy.usernameType === `email`\n    }\n  },\n  watch: {\n    filteredStrategies (newValue, oldValue) {\n      if (_.head(newValue).strategy.useForm) {\n        this.selectedStrategyKey = _.head(newValue).key\n      }\n    },\n    selectedStrategyKey (newValue, oldValue) {\n      this.selectedStrategy = _.find(this.strategies, ['key', newValue])\n      if (this.screen === 'changePwd') {\n        return\n      }\n      this.screen = 'login'\n      if (!this.selectedStrategy.strategy.useForm) {\n        this.isLoading = true\n        window.location.assign('/login/' + newValue)\n      } else {\n        this.$nextTick(() => {\n          this.$refs.iptEmail.focus()\n        })\n      }\n    }\n  },\n  mounted () {\n    this.isShown = true\n    if (this.changePwdContinuationToken) {\n      this.screen = 'changePwd'\n      this.continuationToken = this.changePwdContinuationToken\n    }\n  },\n  methods: {\n    /**\n     * LOGIN\n     */\n    async login () {\n      this.errorShown = false\n      if (this.username.length < 2) {\n        this.errorMessage = this.$t('auth:invalidEmailUsername')\n        this.errorShown = true\n        this.$refs.iptEmail.focus()\n      } else if (this.password.length < 2) {\n        this.errorMessage = this.$t('auth:invalidPassword')\n        this.errorShown = true\n        this.$refs.iptPassword.focus()\n      } else {\n        this.loaderColor = 'grey darken-4'\n        this.loaderTitle = this.$t('auth:signingIn')\n        this.isLoading = true\n        try {\n          const resp = await this.$apollo.mutate({\n            mutation: gql`\n              mutation($username: String!, $password: String!, $strategy: String!) {\n                authentication {\n                  login(username: $username, password: $password, strategy: $strategy) {\n                    responseResult {\n                      succeeded\n                      errorCode\n                      slug\n                      message\n                    }\n                    jwt\n                    mustChangePwd\n                    mustProvideTFA\n                    mustSetupTFA\n                    continuationToken\n                    redirect\n                    tfaQRImage\n                  }\n                }\n              }\n            `,\n            variables: {\n              username: this.username,\n              password: this.password,\n              strategy: this.selectedStrategy.key\n            }\n          })\n          if (_.has(resp, 'data.authentication.login')) {\n            const respObj = _.get(resp, 'data.authentication.login', {})\n            if (respObj.responseResult.succeeded === true) {\n              this.handleLoginResponse(respObj)\n            } else {\n              throw new Error(respObj.responseResult.message)\n            }\n          } else {\n            throw new Error(this.$t('auth:genericError'))\n          }\n        } catch (err) {\n          console.error(err)\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: err.message,\n            icon: 'alert'\n          })\n          this.isLoading = false\n        }\n      }\n    },\n    /**\n     * VERIFY TFA CODE\n     */\n    async verifySecurityCode (setup = false) {\n      if (this.securityCode.length !== 6) {\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: 'Enter a valid security code.',\n          icon: 'alert'\n        })\n        if (setup) {\n          this.$refs.iptTFASetup.focus()\n        } else {\n          this.$refs.iptTFA.focus()\n        }\n      } else {\n        this.loaderColor = 'grey darken-4'\n        this.loaderTitle = this.$t('auth:signingIn')\n        this.isLoading = true\n        try {\n          const resp = await this.$apollo.mutate({\n            mutation: gql`\n              mutation(\n                $continuationToken: String!\n                $securityCode: String!\n                $setup: Boolean\n                ) {\n                authentication {\n                  loginTFA(\n                    continuationToken: $continuationToken\n                    securityCode: $securityCode\n                    setup: $setup\n                    ) {\n                    responseResult {\n                      succeeded\n                      errorCode\n                      slug\n                      message\n                    }\n                    jwt\n                    mustChangePwd\n                    continuationToken\n                    redirect\n                  }\n                }\n              }\n            `,\n            variables: {\n              continuationToken: this.continuationToken,\n              securityCode: this.securityCode,\n              setup\n            }\n          })\n          if (_.has(resp, 'data.authentication.loginTFA')) {\n            let respObj = _.get(resp, 'data.authentication.loginTFA', {})\n            if (respObj.responseResult.succeeded === true) {\n              this.handleLoginResponse(respObj)\n            } else {\n              if (!setup) {\n                this.isTFAShown = false\n              }\n              throw new Error(respObj.responseResult.message)\n            }\n          } else {\n            throw new Error(this.$t('auth:genericError'))\n          }\n        } catch (err) {\n          console.error(err)\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: err.message,\n            icon: 'alert'\n          })\n          this.isLoading = false\n        }\n      }\n    },\n    /**\n     * CHANGE PASSWORD\n     */\n    async changePassword () {\n      this.loaderColor = 'grey darken-4'\n      this.loaderTitle = this.$t('auth:changePwd.loading')\n      this.isLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $continuationToken: String!\n              $newPassword: String!\n            ) {\n              authentication {\n                loginChangePassword (\n                  continuationToken: $continuationToken\n                  newPassword: $newPassword\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                  jwt\n                  continuationToken\n                  redirect\n                }\n              }\n            }\n          `,\n          variables: {\n            continuationToken: this.continuationToken,\n            newPassword: this.newPassword\n          }\n        })\n        if (_.has(resp, 'data.authentication.loginChangePassword')) {\n          let respObj = _.get(resp, 'data.authentication.loginChangePassword', {})\n          if (respObj.responseResult.succeeded === true) {\n            this.handleLoginResponse(respObj)\n          } else {\n            throw new Error(respObj.responseResult.message)\n          }\n        } else {\n          throw new Error(this.$t('auth:genericError'))\n        }\n      } catch (err) {\n        console.error(err)\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n        this.isLoading = false\n      }\n    },\n    /**\n     * SWITCH TO FORGOT PASSWORD SCREEN\n     */\n    forgotPassword () {\n      this.screen = 'forgot'\n      this.$nextTick(() => {\n        this.$refs.iptForgotPwdEmail.focus()\n      })\n    },\n    /**\n     * FORGOT PASSWORD SUBMIT\n     */\n    async forgotPasswordSubmit () {\n      this.loaderColor = 'grey darken-4'\n      this.loaderTitle = this.$t('auth:forgotPasswordLoading')\n      this.isLoading = true\n      try {\n        const resp = await this.$apollo.mutate({\n          mutation: gql`\n            mutation (\n              $email: String!\n            ) {\n              authentication {\n                forgotPassword (\n                  email: $email\n                ) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                }\n              }\n            }\n          `,\n          variables: {\n            email: this.username\n          }\n        })\n        if (_.has(resp, 'data.authentication.forgotPassword.responseResult')) {\n          let respObj = _.get(resp, 'data.authentication.forgotPassword.responseResult', {})\n          if (respObj.succeeded === true) {\n            this.$store.commit('showNotification', {\n              style: 'success',\n              message: this.$t('auth:forgotPasswordSuccess'),\n              icon: 'email'\n            })\n            this.screen = 'login'\n          } else {\n            throw new Error(respObj.message)\n          }\n        } else {\n          throw new Error(this.$t('auth:genericError'))\n        }\n      } catch (err) {\n        console.error(err)\n        this.$store.commit('showNotification', {\n          style: 'red',\n          message: err.message,\n          icon: 'alert'\n        })\n      }\n      this.isLoading = false\n    },\n    handleLoginResponse (respObj) {\n      this.continuationToken = respObj.continuationToken\n      if (respObj.mustChangePwd === true) {\n        this.screen = 'changePwd'\n        this.$nextTick(() => {\n          this.$refs.iptNewPassword.focus()\n        })\n        this.isLoading = false\n      } else if (respObj.mustProvideTFA === true) {\n        this.securityCode = ''\n        this.isTFAShown = true\n        setTimeout(() => {\n          this.$refs.iptTFA.focus()\n        }, 500)\n        this.isLoading = false\n      } else if (respObj.mustSetupTFA === true) {\n        this.securityCode = ''\n        this.isTFASetupShown = true\n        this.tfaQRImage = respObj.tfaQRImage\n        setTimeout(() => {\n          this.$refs.iptTFASetup.focus()\n        }, 500)\n        this.isLoading = false\n      } else {\n        this.loaderColor = 'green darken-1'\n        this.loaderTitle = this.$t('auth:loginSuccess')\n        Cookies.set('jwt', respObj.jwt, { expires: 365, secure: window.location.protocol === 'https:' })\n        _.delay(() => {\n          const loginRedirect = Cookies.get('loginRedirect')\n          const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://')\n          if (loginRedirect === '/' && respObj.redirect) {\n            Cookies.remove('loginRedirect')\n            window.location.replace(respObj.redirect)\n          } else if (isValidRedirect) {\n            Cookies.remove('loginRedirect')\n            window.location.replace(loginRedirect)\n          } else {\n            if (loginRedirect) {\n              Cookies.remove('loginRedirect')\n            }\n            if (respObj.redirect) {\n              window.location.replace(respObj.redirect)\n            } else {\n              window.location.replace('/')\n            }\n          }\n        }, 1000)\n      }\n    }\n  },\n  apollo: {\n    strategies: {\n      query: gql`\n        {\n          authentication {\n            activeStrategies(enabledOnly: true) {\n              key\n              strategy {\n                key\n                logo\n                color\n                icon\n                useForm\n                usernameType\n              }\n              displayName\n              order\n              selfRegistration\n            }\n          }\n        }\n      `,\n      update: (data) => _.sortBy(data.authentication.activeStrategies, ['order']),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n  .login {\n    // background-image: url('/_assets/img/splash/1.jpg');\n    background-color: mc('grey', '900');\n    background-size: cover;\n    background-position: center center;\n    width: 100%;\n    height: 100%;\n\n    &-sd {\n      background-color: rgba(255,255,255,.8);\n      backdrop-filter: blur(10px);\n      -webkit-backdrop-filter: blur(10px);\n      border-left: 1px solid rgba(255,255,255,.85);\n      border-right: 1px solid rgba(255,255,255,.85);\n      width: 450px;\n      height: 100%;\n      margin-left: 5vw;\n\n      @at-root .no-backdropfilter & {\n        background-color: rgba(255,255,255,.95);\n      }\n\n      @include until($tablet) {\n        margin-left: 0;\n        width: 100%;\n      }\n    }\n\n    &-logo {\n      padding: 12px 0 0 12px;\n      width: 58px;\n      height: 58px;\n      background-color: #222;\n      margin-left: 12px;\n      border-bottom-left-radius: 7px;\n      border-bottom-right-radius: 7px;\n    }\n\n    &-title {\n      height: 58px;\n      padding-left: 12px;\n      display: flex;\n      align-items: center;\n      text-shadow: .5px .5px #FFF;\n    }\n\n    &-subtitle {\n      padding: 24px 12px 12px 12px;\n      color: #111;\n      font-weight: 500;\n      text-shadow: 1px 1px rgba(255,255,255,.5);\n      background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));\n      text-align: center;\n      border-bottom: 1px solid rgba(0,0,0,.3);\n    }\n\n    &-info {\n      border-top: 1px solid rgba(255,255,255,.85);\n      background-color: rgba(255,255,255,.15);\n      border-bottom: 1px solid rgba(0,0,0,.15);\n      padding: 12px;\n      font-size: 13px;\n      text-align: center;\n      color: mc('grey', '900');\n    }\n\n    &-list {\n      border-top: 1px solid rgba(255,255,255,.85);\n      padding: 12px;\n    }\n\n    &-form {\n      padding: 12px;\n      border-top: 1px solid rgba(255,255,255,.85);\n    }\n\n    &-main {\n      flex: 1 0 100vw;\n      height: 100vh;\n    }\n\n    &-tfa {\n      background-color: #EEE;\n      border: 7px solid #FFF;\n\n      &-field input {\n        text-align: center;\n      }\n\n      &-qr {\n        background-color: #FFF;\n        padding: 5px;\n        border-radius: 5px;\n        width: 200px;\n        height: 200px;\n        margin: 0 auto;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "client/components/new-page.vue",
    "content": "<template lang='pug'>\n  v-app\n    .newpage\n      .newpage-content\n        img.animated.fadeIn(src='/_assets/svg/icon-delete-file.svg', alt='Not Found')\n        .headline {{ $t('newpage.title') }}\n        .subtitle-1.mt-3 {{ $t('newpage.subtitle') }}\n        v-btn.mt-5(:href='`/e/` + locale + `/` + path', x-large)\n          v-icon(left) mdi-plus\n          span {{ $t('newpage.create') }}\n        v-btn.mt-5(color='purple lighten-3', href='javascript:window.history.go(-1);', outlined)\n          v-icon(left) mdi-arrow-left\n          span {{ $t('newpage.goback') }}\n</template>\n\n<script>\n\nexport default {\n  props: {\n    locale: {\n      type: String,\n      default: 'en'\n    },\n    path: {\n      type: String,\n      default: 'home'\n    }\n  },\n  data() {\n    return { }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/not-found.vue",
    "content": "<template lang='pug'>\n  v-app\n    .notfound\n      .notfound-content\n        img.animated.fadeIn(src='/_assets/svg/icon-delete-file.svg', alt='Not Found')\n        .headline {{$t('notfound.title')}}\n        .subheading.mt-3 {{$t('notfound.subtitle')}}\n        v-btn.mt-5(color='red lighten-4', href='/', large, outlined)\n          v-icon(left) mdi-home\n          span {{$t('notfound.gohome')}}\n</template>\n\n<script>\n\nexport default {\n  data() {\n    return { }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/profile/comments.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, fill-height, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .headline.primary--text Comments\n        .subheading.grey--text List of comments I posted\n</template>\n\n<script>\n\nexport default {\n  data() {\n    return { }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/profile/pages.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .profile-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Users', style='width: 80px;')\n          .profile-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('profile:pages.title')}}\n            .subheading.grey--text.animated.fadeInLeft {{$t('profile:pages.subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInDown.wait-p1s(color='grey', outlined, @click='refresh', large)\n            v-icon.grey--text mdi-refresh\n      v-flex(xs12)\n        v-card.animated.fadeInUp\n          v-data-table(\n            :items='pages'\n            :headers='headers'\n            :page.sync='pagination'\n            :items-per-page='15'\n            :loading='loading'\n            must-sort,\n            sort-by='updatedAt',\n            sort-desc,\n            hide-default-footer\n          )\n            template(slot='item', slot-scope='props')\n              tr.is-clickable(:active='props.selected', @click='goToPage(props.item.id)')\n                td\n                  .body-2: strong {{ props.item.title }}\n                  .caption {{ props.item.description }}\n                td.admin-pages-path\n                  v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}\n                  span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}\n                td {{ props.item.createdAt | moment('calendar') }}\n                td {{ props.item.updatedAt | moment('calendar') }}\n            template(slot='no-data')\n              v-alert.ma-3(icon='mdi-alert', :value='true', outlined, color='grey')\n                em.caption {{$t('profile:pages.emptyList')}}\n          .text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')\n            v-pagination(v-model='pagination', :length='pageTotal')\n</template>\n\n<script>\nimport gql from 'graphql-tag'\n\nexport default {\n  data() {\n    return {\n      selectedPage: {},\n      pagination: 1,\n      pages: [],\n      loading: false\n    }\n  },\n  computed: {\n    headers () {\n      return [\n        { text: this.$t('profile:pages.headerTitle'), value: 'title' },\n        { text: this.$t('profile:pages.headerPath'), value: 'path' },\n        { text: this.$t('profile:pages.headerCreatedAt'), value: 'createdAt', width: 250 },\n        { text: this.$t('profile:pages.headerUpdatedAt'), value: 'updatedAt', width: 250 }\n      ]\n    },\n    pageTotal () {\n      return Math.ceil(this.pages.length / 15)\n    }\n  },\n  methods: {\n    async refresh() {\n      await this.$apollo.queries.pages.refetch()\n      this.$store.commit('showNotification', {\n        message: this.$t('profile:pages.refreshSuccess'),\n        style: 'success',\n        icon: 'cached'\n      })\n    },\n    goToPage(id) {\n      window.location.assign(`/i/` + id)\n    }\n  },\n  apollo: {\n    pages: {\n      query: gql`\n        query($creatorId: Int, $authorId: Int) {\n          pages {\n            list(creatorId: $creatorId, authorId: $authorId) {\n              id\n              locale\n              path\n              title\n              description\n              contentType\n              isPublished\n              isPrivate\n              privateNS\n              createdAt\n              updatedAt\n            }\n          }\n        }\n      `,\n      variables () {\n        return {\n          creatorId: this.$store.get('user/id'),\n          authorId: this.$store.get('user/id')\n        }\n      },\n      fetchPolicy: 'network-only',\n      update: (data) => data.pages.list,\n      watchLoading (isLoading) {\n        this.loading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'profile-pages-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/profile/profile.vue",
    "content": "<template lang='pug'>\n  v-container(fluid, grid-list-lg)\n    v-layout(row wrap)\n      v-flex(xs12)\n        .profile-header\n          img.animated.fadeInUp(src='/_assets/svg/icon-profile.svg', alt='Users', style='width: 80px;')\n          .profile-header-title\n            .headline.primary--text.animated.fadeInLeft {{$t('profile:title')}}\n            .subheading.grey--text.animated.fadeInLeft {{$t('profile:subtitle')}}\n          v-spacer\n          v-btn.animated.fadeInDown(color='success', depressed, @click='saveProfile', :loading='saveLoading', large)\n            v-icon(left) mdi-check\n            span {{$t('common:actions.save')}}\n          //- v-btn.animated.fadeInDown(outlined, color='primary', disabled).mr-0\n          //-   v-icon(left) mdi-earth\n          //-   span {{$t('profile:viewPublicProfile')}}\n      v-flex(lg6 xs12)\n        v-card.animated.fadeInUp\n          v-toolbar(color='blue-grey', dark, dense, flat)\n            v-toolbar-title.subtitle-1 {{$t('profile:myInfo')}}\n          v-list(two-line, dense)\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-account\n              v-list-item-content\n                v-list-item-title {{$t('profile:displayName')}}\n                v-list-item-subtitle {{ user.name }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.name'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDisplayName`)')\n                      v-icon(left) mdi-pencil\n                      span {{ $t('common:actions:edit') }}\n                  v-card\n                    v-text-field(\n                      ref='iptDisplayName'\n                      v-model='user.name'\n                      :label='$t(`profile:displayName`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.name = false'\n                      @keydown.enter='editPop.name = false'\n                      @keydown.esc='editPop.name = false'\n                    )\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-map-marker\n              v-list-item-content\n                v-list-item-title {{$t('profile:location')}}\n                v-list-item-subtitle {{ user.location }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.location'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptLocation`)')\n                      v-icon(left) mdi-pencil\n                      span {{ $t('common:actions:edit') }}\n                  v-card\n                    v-text-field(\n                      ref='iptLocation'\n                      v-model='user.location'\n                      :label='$t(`profile:location`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.location = false'\n                      @keydown.enter='editPop.location = false'\n                      @keydown.esc='editPop.location = false'\n                    )\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-briefcase\n              v-list-item-content\n                v-list-item-title {{$t('profile:jobTitle')}}\n                v-list-item-subtitle {{ user.jobTitle }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.jobTitle'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptJobTitle`)')\n                      v-icon(left) mdi-pencil\n                      span {{ $t('common:actions:edit') }}\n                  v-card\n                    v-text-field(\n                      ref='iptJobTitle'\n                      v-model='user.jobTitle'\n                      :label='$t(`profile:jobTitle`)'\n                      solo\n                      hide-details\n                      append-icon='mdi-check'\n                      @click:append='editPop.jobTitle = false'\n                      @keydown.enter='editPop.jobTitle = false'\n                      @keydown.esc='editPop.jobTitle = false'\n                    )\n\n        v-card.mt-3.animated.fadeInUp.wait-p2s\n          v-toolbar(color='blue-grey', dark, dense, flat)\n            v-toolbar-title\n              .subtitle-1 {{$t('profile:auth.title')}}\n          v-card-text.pt-0\n            v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.provider')}}\n            v-toolbar(\n              flat\n              :color='$vuetify.theme.dark ? \"grey darken-2\" : \"purple lighten-5\"'\n              dense\n              :class='$vuetify.theme.dark ? \"grey--text text--lighten-1\" : \"purple--text text--darken-4\"'\n              )\n              v-icon(:color='$vuetify.theme.dark ? \"grey lighten-1\" : \"purple darken-4\"') mdi-shield-lock\n              .subheading.ml-3 {{ user.providerName }}\n            //- v-divider.mt-3\n            //- v-subheader.pl-0: span.subtitle-2 Two-Factor Authentication (2FA)\n            //- .caption.mb-2 2FA adds an extra layer of security by requiring a unique code generated on your smartphone when signing in.\n            //- v-btn(color='purple darken-4', disabled).ml-0 Enable 2FA\n            //- v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA\n            template(v-if='user.providerKey === `local`')\n              form#change-password-form(@submit.prevent='changePassword')\n                v-divider.mt-3\n                v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}}\n                v-text-field(\n                  ref='iptCurrentPass'\n                  v-model='currentPass'\n                  outlined\n                  :label='$t(`profile:auth.currentPassword`)'\n                  type='password'\n                  prepend-inner-icon='mdi-form-textbox-password'\n                  autocomplete='current-password'\n                  )\n                v-text-field(\n                  ref='iptNewPass'\n                  v-model='newPass'\n                  outlined\n                  :label='$t(`profile:auth.newPassword`)'\n                  type='password'\n                  prepend-inner-icon='mdi-form-textbox-password'\n                  autocomplete='off'\n                  counter='255'\n                  loading\n                  )\n                  password-strength(slot='progress', v-model='newPass')\n                v-text-field(\n                  ref='iptVerifyPass'\n                  v-model='verifyPass'\n                  outlined\n                  :label='$t(`profile:auth.verifyPassword`)'\n                  type='password'\n                  prepend-inner-icon='mdi-form-textbox-password'\n                  autocomplete='off'\n                  hide-details\n                  )\n          v-card-chin(v-if='user.providerKey === `local`')\n            v-spacer\n            v-btn.px-4(color='purple darken-4', dark, depressed, :loading='changePassLoading', type='submit', form='change-password-form')\n              v-icon(left) mdi-progress-check\n              span {{$t('profile:auth.changePassword')}}\n      v-flex(lg6 xs12)\n        //- v-card\n        //-   v-toolbar(color='blue-grey', dark, dense, flat)\n        //-     v-toolbar-title\n        //-       .subtitle-1 Picture\n        //-   v-card-title\n        //-     v-avatar.blue(v-if='picture.kind === `initials`', :size='40')\n        //-       span.white--text.subheading {{picture.initials}}\n        //-     v-avatar(v-else-if='picture.kind === `image`', :size='40')\n        //-       v-img(:src='picture.url')\n        //-     v-btn(outlined).mx-4 Upload Picture\n        //-     v-btn(outlined, disabled) Remove Picture\n        v-card.animated.fadeInUp.wait-p2s\n          v-toolbar(color='blue-grey', dark, dense, flat)\n            v-toolbar-title.subtitle-1 {{$t('profile:preferences')}}\n          v-list(two-line, dense)\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-map-clock-outline\n              v-list-item-content\n                v-list-item-title {{$t('profile:timezone')}}\n                v-list-item-subtitle {{ user.timezone }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.timezone'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  max-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptTimezone`)')\n                      v-icon(left) mdi-pencil\n                      span {{ $t('common:actions:edit') }}\n                  v-card(flat)\n                    v-select(\n                      ref='iptTimezone'\n                      :items='timezones'\n                      v-model='user.timezone'\n                      :label='$t(`profile:timezone`)'\n                      solo\n                      flat\n                      dense\n                      hide-details\n                      @keydown.enter='editPop.timezone = false'\n                      @keydown.esc='editPop.timezone = false'\n                      style='height: 38px;'\n                    )\n                    v-card-chin\n                      v-spacer\n                      v-btn(\n                        small\n                        text\n                        color='primary'\n                        @click='editPop.timezone = false'\n                        )\n                        v-icon(left) mdi-check\n                        span {{$t('common:actions.ok')}}\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-calendar-month-outline\n              v-list-item-content\n                v-list-item-title {{$t('profile:dateFormat')}}\n                v-list-item-subtitle {{ user.dateFormat && user.dateFormat.length > 0 ? user.dateFormat : $t('profile:localeDefault') }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.dateFormat'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  max-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDateFormat`)')\n                      v-icon(left) mdi-pencil\n                      span {{ $t('common:actions:edit') }}\n                  v-card(flat)\n                    v-select(\n                      ref='iptDateFormat'\n                      :items='dateFormats'\n                      v-model='user.dateFormat'\n                      :label='$t(`profile:dateFormat`)'\n                      solo\n                      flat\n                      dense\n                      hide-details\n                      @keydown.enter='editPop.dateFormat = false'\n                      @keydown.esc='editPop.dateFormat = false'\n                      style='height: 38px;'\n                    )\n                    v-card-chin\n                      v-spacer\n                      v-btn(\n                        small\n                        text\n                        color='primary'\n                        @click='editPop.dateFormat = false'\n                        )\n                        v-icon(left) mdi-check\n                        span {{$t('common:actions.ok')}}\n            v-divider\n            v-list-item\n              v-list-item-avatar(size='32')\n                v-icon mdi-palette\n              v-list-item-content\n                v-list-item-title {{$t('profile:appearance')}}\n                v-list-item-subtitle {{ currentAppearance }}\n              v-list-item-action\n                v-menu(\n                  v-model='editPop.appearance'\n                  :close-on-content-click='false'\n                  min-width='350'\n                  max-width='350'\n                  left\n                  )\n                  template(v-slot:activator='{ on }')\n                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptAppearance`)')\n                      v-icon(left) mdi-pencil\n                      span {{ $t('common:actions:edit') }}\n                  v-card(flat)\n                    v-select(\n                      ref='iptAppearance'\n                      :items='appearances'\n                      v-model='user.appearance'\n                      :label='$t(`profile:appearance`)'\n                      solo\n                      flat\n                      dense\n                      hide-details\n                      @keydown.enter='editPop.appearance = false'\n                      @keydown.esc='editPop.appearance = false'\n                      style='height: 38px;'\n                    )\n                    v-card-chin\n                      v-spacer\n                      v-btn(\n                        small\n                        text\n                        color='primary'\n                        @click='editPop.appearance = false'\n                        )\n                        v-icon(left) mdi-check\n                        span {{$t('common:actions.ok')}}\n\n        v-card.mt-3.animated.fadeInUp.wait-p3s\n          v-toolbar(color='primary', dark, dense, flat)\n            v-toolbar-title\n              .subtitle-1 {{$t('profile:groups.title')}}\n          v-list(dense)\n            template(v-for='(grp, idx) of user.groups')\n              v-list-item(:key='`grp-id-` + grp')\n                v-list-item-avatar(size='32')\n                  v-icon mdi-account-group\n                v-list-item-content\n                  v-list-item-title.body-2 {{grp}}\n              v-divider(v-if='idx < user.groups.length - 1')\n\n        v-card.mt-3.animated.fadeInUp.wait-p4s\n          v-toolbar(color='teal', dark, dense, flat)\n            v-toolbar-title\n              .subtitle-1 {{$t('profile:activity.title')}}\n          v-card-text.grey--text.text--darken-2\n            .caption.grey--text {{$t('profile:activity.joinedOn')}}\n            .body-2: strong {{ user.createdAt | moment('LLLL') }}\n            .caption.grey--text.mt-3 {{$t('profile:activity.lastUpdatedOn')}}\n            .body-2: strong {{ user.updatedAt | moment('LLLL') }}\n            .caption.grey--text.mt-3 {{$t('profile:activity.lastLoginOn')}}\n            .body-2: strong {{ user.lastLoginAt | moment('LLLL') }}\n            v-divider.mt-3\n            .caption.grey--text.mt-3 {{$t('profile:activity.pagesCreated')}}\n            .body-2: strong {{ user.pagesTotal }}\n            .caption.grey--text.mt-3 {{$t('profile:activity.commentsPosted')}}\n            .body-2: strong 0\n</template>\n\n<script>\nimport { get } from 'vuex-pathify'\nimport gql from 'graphql-tag'\nimport _ from 'lodash'\nimport Cookies from 'js-cookie'\nimport validate from 'validate.js'\n\nimport PasswordStrength from '../common/password-strength.vue'\n\n/* global WIKI, siteConfig */\n\nexport default {\n  i18nOptions: {\n    namespaces: ['profile', 'auth']\n  },\n  components: {\n    PasswordStrength\n  },\n  data() {\n    return {\n      saveLoading: false,\n      changePassLoading: false,\n      user: {\n        name: 'unknown',\n        location: '',\n        jobTitle: '',\n        timezone: '',\n        dateFormat: '',\n        appearance: '',\n        createdAt: '1970-01-01',\n        updatedAt: '1970-01-01',\n        lastLoginAt: '1970-01-01',\n        groups: []\n      },\n      currentPass: '',\n      newPass: '',\n      verifyPass: '',\n      editPop: {\n        name: false,\n        location: false,\n        jobTitle: false,\n        timezone: false,\n        dateFormat: false,\n        appearance: false\n      },\n      timezones: [\n        { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },\n        { text: '(GMT-11:00) Pago Pago', value: 'Pacific/Pago_Pago' },\n        { text: '(GMT-10:00) Hawaii Time', value: 'Pacific/Honolulu' },\n        { text: '(GMT-10:00) Rarotonga', value: 'Pacific/Rarotonga' },\n        { text: '(GMT-10:00) Tahiti', value: 'Pacific/Tahiti' },\n        { text: '(GMT-09:30) Marquesas', value: 'Pacific/Marquesas' },\n        { text: '(GMT-09:00) Alaska Time', value: 'America/Anchorage' },\n        { text: '(GMT-09:00) Gambier', value: 'Pacific/Gambier' },\n        { text: '(GMT-08:00) Pacific Time', value: 'America/Los_Angeles' },\n        { text: '(GMT-08:00) Pacific Time - Tijuana', value: 'America/Tijuana' },\n        { text: '(GMT-08:00) Pacific Time - Vancouver', value: 'America/Vancouver' },\n        { text: '(GMT-08:00) Pacific Time - Whitehorse', value: 'America/Whitehorse' },\n        { text: '(GMT-08:00) Pitcairn', value: 'Pacific/Pitcairn' },\n        { text: '(GMT-07:00) Mountain Time', value: 'America/Denver' },\n        { text: '(GMT-07:00) Mountain Time - Arizona', value: 'America/Phoenix' },\n        { text: '(GMT-07:00) Mountain Time - Chihuahua, Mazatlan', value: 'America/Mazatlan' },\n        { text: '(GMT-07:00) Mountain Time - Dawson Creek', value: 'America/Dawson_Creek' },\n        { text: '(GMT-07:00) Mountain Time - Edmonton', value: 'America/Edmonton' },\n        { text: '(GMT-07:00) Mountain Time - Hermosillo', value: 'America/Hermosillo' },\n        { text: '(GMT-07:00) Mountain Time - Yellowknife', value: 'America/Yellowknife' },\n        { text: '(GMT-06:00) Belize', value: 'America/Belize' },\n        { text: '(GMT-06:00) Central Time', value: 'America/Chicago' },\n        { text: '(GMT-06:00) Central Time - Mexico City', value: 'America/Mexico_City' },\n        { text: '(GMT-06:00) Central Time - Regina', value: 'America/Regina' },\n        { text: '(GMT-06:00) Central Time - Tegucigalpa', value: 'America/Tegucigalpa' },\n        { text: '(GMT-06:00) Central Time - Winnipeg', value: 'America/Winnipeg' },\n        { text: '(GMT-06:00) Costa Rica', value: 'America/Costa_Rica' },\n        { text: '(GMT-06:00) El Salvador', value: 'America/El_Salvador' },\n        { text: '(GMT-06:00) Galapagos', value: 'Pacific/Galapagos' },\n        { text: '(GMT-06:00) Guatemala', value: 'America/Guatemala' },\n        { text: '(GMT-06:00) Managua', value: 'America/Managua' },\n        { text: '(GMT-05:00) America Cancun', value: 'America/Cancun' },\n        { text: '(GMT-05:00) Bogota', value: 'America/Bogota' },\n        { text: '(GMT-05:00) Easter Island', value: 'Pacific/Easter' },\n        { text: '(GMT-05:00) Eastern Time', value: 'America/New_York' },\n        { text: '(GMT-05:00) Eastern Time - Iqaluit', value: 'America/Iqaluit' },\n        { text: '(GMT-05:00) Eastern Time - Toronto', value: 'America/Toronto' },\n        { text: '(GMT-05:00) Guayaquil', value: 'America/Guayaquil' },\n        { text: '(GMT-05:00) Havana', value: 'America/Havana' },\n        { text: '(GMT-05:00) Jamaica', value: 'America/Jamaica' },\n        { text: '(GMT-05:00) Lima', value: 'America/Lima' },\n        { text: '(GMT-05:00) Nassau', value: 'America/Nassau' },\n        { text: '(GMT-05:00) Panama', value: 'America/Panama' },\n        { text: '(GMT-05:00) Port-au-Prince', value: 'America/Port-au-Prince' },\n        { text: '(GMT-05:00) Rio Branco', value: 'America/Rio_Branco' },\n        { text: '(GMT-04:00) Atlantic Time - Halifax', value: 'America/Halifax' },\n        { text: '(GMT-04:00) Barbados', value: 'America/Barbados' },\n        { text: '(GMT-04:00) Bermuda', value: 'Atlantic/Bermuda' },\n        { text: '(GMT-04:00) Boa Vista', value: 'America/Boa_Vista' },\n        { text: '(GMT-04:00) Caracas', value: 'America/Caracas' },\n        { text: '(GMT-04:00) Curacao', value: 'America/Curacao' },\n        { text: '(GMT-04:00) Grand Turk', value: 'America/Grand_Turk' },\n        { text: '(GMT-04:00) Guyana', value: 'America/Guyana' },\n        { text: '(GMT-04:00) La Paz', value: 'America/La_Paz' },\n        { text: '(GMT-04:00) Manaus', value: 'America/Manaus' },\n        { text: '(GMT-04:00) Martinique', value: 'America/Martinique' },\n        { text: '(GMT-04:00) Port of Spain', value: 'America/Port_of_Spain' },\n        { text: '(GMT-04:00) Porto Velho', value: 'America/Porto_Velho' },\n        { text: '(GMT-04:00) Puerto Rico', value: 'America/Puerto_Rico' },\n        { text: '(GMT-04:00) Santo Domingo', value: 'America/Santo_Domingo' },\n        { text: '(GMT-04:00) Thule', value: 'America/Thule' },\n        { text: '(GMT-03:30) Newfoundland Time - St. Johns', value: 'America/St_Johns' },\n        { text: '(GMT-03:00) Araguaina', value: 'America/Araguaina' },\n        { text: '(GMT-03:00) Asuncion', value: 'America/Asuncion' },\n        { text: '(GMT-03:00) Belem', value: 'America/Belem' },\n        { text: '(GMT-03:00) Buenos Aires', value: 'America/Argentina/Buenos_Aires' },\n        { text: '(GMT-03:00) Campo Grande', value: 'America/Campo_Grande' },\n        { text: '(GMT-03:00) Cayenne', value: 'America/Cayenne' },\n        { text: '(GMT-03:00) Cuiaba', value: 'America/Cuiaba' },\n        { text: '(GMT-03:00) Fortaleza', value: 'America/Fortaleza' },\n        { text: '(GMT-03:00) Godthab', value: 'America/Godthab' },\n        { text: '(GMT-03:00) Maceio', value: 'America/Maceio' },\n        { text: '(GMT-03:00) Miquelon', value: 'America/Miquelon' },\n        { text: '(GMT-03:00) Montevideo', value: 'America/Montevideo' },\n        { text: '(GMT-03:00) Palmer', value: 'Antarctica/Palmer' },\n        { text: '(GMT-03:00) Paramaribo', value: 'America/Paramaribo' },\n        { text: '(GMT-03:00) Punta Arenas', value: 'America/Punta_Arenas' },\n        { text: '(GMT-03:00) Recife', value: 'America/Recife' },\n        { text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },\n        { text: '(GMT-03:00) Salvador', value: 'America/Bahia' },\n        { text: '(GMT-03:00) Santiago', value: 'America/Santiago' },\n        { text: '(GMT-03:00) Sao Paulo', value: 'America/Sao_Paulo' },\n        { text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },\n        { text: '(GMT-02:00) Noronha', value: 'America/Noronha' },\n        { text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },\n        { text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },\n        { text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },\n        { text: '(GMT-01:00) Scoresbysund', value: 'America/Scoresbysund' },\n        { text: '(GMT+00:00) Abidjan', value: 'Africa/Abidjan' },\n        { text: '(GMT+00:00) Accra', value: 'Africa/Accra' },\n        { text: '(GMT+00:00) Bissau', value: 'Africa/Bissau' },\n        { text: '(GMT+00:00) Canary Islands', value: 'Atlantic/Canary' },\n        { text: '(GMT+00:00) Casablanca', value: 'Africa/Casablanca' },\n        { text: '(GMT+00:00) Danmarkshavn', value: 'America/Danmarkshavn' },\n        { text: '(GMT+00:00) Dublin', value: 'Europe/Dublin' },\n        { text: '(GMT+00:00) El Aaiun', value: 'Africa/El_Aaiun' },\n        { text: '(GMT+00:00) Faeroe', value: 'Atlantic/Faroe' },\n        { text: '(GMT+00:00) GMT (no daylight saving)', value: 'Etc/GMT' },\n        { text: '(GMT+00:00) Lisbon', value: 'Europe/Lisbon' },\n        { text: '(GMT+00:00) London', value: 'Europe/London' },\n        { text: '(GMT+00:00) Monrovia', value: 'Africa/Monrovia' },\n        { text: '(GMT+00:00) Reykjavik', value: 'Atlantic/Reykjavik' },\n        { text: '(GMT+01:00) Algiers', value: 'Africa/Algiers' },\n        { text: '(GMT+01:00) Amsterdam', value: 'Europe/Amsterdam' },\n        { text: '(GMT+01:00) Andorra', value: 'Europe/Andorra' },\n        { text: '(GMT+01:00) Berlin', value: 'Europe/Berlin' },\n        { text: '(GMT+01:00) Brussels', value: 'Europe/Brussels' },\n        { text: '(GMT+01:00) Budapest', value: 'Europe/Budapest' },\n        { text: '(GMT+01:00) Central European Time - Belgrade', value: 'Europe/Belgrade' },\n        { text: '(GMT+01:00) Central European Time - Prague', value: 'Europe/Prague' },\n        { text: '(GMT+01:00) Ceuta', value: 'Africa/Ceuta' },\n        { text: '(GMT+01:00) Copenhagen', value: 'Europe/Copenhagen' },\n        { text: '(GMT+01:00) Gibraltar', value: 'Europe/Gibraltar' },\n        { text: '(GMT+01:00) Lagos', value: 'Africa/Lagos' },\n        { text: '(GMT+01:00) Luxembourg', value: 'Europe/Luxembourg' },\n        { text: '(GMT+01:00) Madrid', value: 'Europe/Madrid' },\n        { text: '(GMT+01:00) Malta', value: 'Europe/Malta' },\n        { text: '(GMT+01:00) Monaco', value: 'Europe/Monaco' },\n        { text: '(GMT+01:00) Ndjamena', value: 'Africa/Ndjamena' },\n        { text: '(GMT+01:00) Oslo', value: 'Europe/Oslo' },\n        { text: '(GMT+01:00) Paris', value: 'Europe/Paris' },\n        { text: '(GMT+01:00) Rome', value: 'Europe/Rome' },\n        { text: '(GMT+01:00) Stockholm', value: 'Europe/Stockholm' },\n        { text: '(GMT+01:00) Tirane', value: 'Europe/Tirane' },\n        { text: '(GMT+01:00) Tunis', value: 'Africa/Tunis' },\n        { text: '(GMT+01:00) Vienna', value: 'Europe/Vienna' },\n        { text: '(GMT+01:00) Warsaw', value: 'Europe/Warsaw' },\n        { text: '(GMT+01:00) Zurich', value: 'Europe/Zurich' },\n        { text: '(GMT+02:00) Amman', value: 'Asia/Amman' },\n        { text: '(GMT+02:00) Athens', value: 'Europe/Athens' },\n        { text: '(GMT+02:00) Beirut', value: 'Asia/Beirut' },\n        { text: '(GMT+02:00) Bucharest', value: 'Europe/Bucharest' },\n        { text: '(GMT+02:00) Cairo', value: 'Africa/Cairo' },\n        { text: '(GMT+02:00) Chisinau', value: 'Europe/Chisinau' },\n        { text: '(GMT+02:00) Damascus', value: 'Asia/Damascus' },\n        { text: '(GMT+02:00) Gaza', value: 'Asia/Gaza' },\n        { text: '(GMT+02:00) Helsinki', value: 'Europe/Helsinki' },\n        { text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },\n        { text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },\n        { text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },\n        { text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },\n        { text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },\n        { text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },\n        { text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },\n        { text: '(GMT+02:00) Riga', value: 'Europe/Riga' },\n        { text: '(GMT+02:00) Sofia', value: 'Europe/Sofia' },\n        { text: '(GMT+02:00) Tallinn', value: 'Europe/Tallinn' },\n        { text: '(GMT+02:00) Tripoli', value: 'Africa/Tripoli' },\n        { text: '(GMT+02:00) Vilnius', value: 'Europe/Vilnius' },\n        { text: '(GMT+02:00) Windhoek', value: 'Africa/Windhoek' },\n        { text: '(GMT+03:00) Baghdad', value: 'Asia/Baghdad' },\n        { text: '(GMT+03:00) Istanbul', value: 'Europe/Istanbul' },\n        { text: '(GMT+03:00) Minsk', value: 'Europe/Minsk' },\n        { text: '(GMT+03:00) Moscow+00 - Moscow', value: 'Europe/Moscow' },\n        { text: '(GMT+03:00) Nairobi', value: 'Africa/Nairobi' },\n        { text: '(GMT+03:00) Qatar', value: 'Asia/Qatar' },\n        { text: '(GMT+03:00) Riyadh', value: 'Asia/Riyadh' },\n        { text: '(GMT+03:00) Syowa', value: 'Antarctica/Syowa' },\n        { text: '(GMT+03:30) Tehran', value: 'Asia/Tehran' },\n        { text: '(GMT+04:00) Baku', value: 'Asia/Baku' },\n        { text: '(GMT+04:00) Dubai', value: 'Asia/Dubai' },\n        { text: '(GMT+04:00) Mahe', value: 'Indian/Mahe' },\n        { text: '(GMT+04:00) Mauritius', value: 'Indian/Mauritius' },\n        { text: '(GMT+04:00) Moscow+01 - Samara', value: 'Europe/Samara' },\n        { text: '(GMT+04:00) Reunion', value: 'Indian/Reunion' },\n        { text: '(GMT+04:00) Tbilisi', value: 'Asia/Tbilisi' },\n        { text: '(GMT+04:00) Yerevan', value: 'Asia/Yerevan' },\n        { text: '(GMT+04:30) Kabul', value: 'Asia/Kabul' },\n        { text: '(GMT+05:00) Aqtau', value: 'Asia/Aqtau' },\n        { text: '(GMT+05:00) Aqtobe', value: 'Asia/Aqtobe' },\n        { text: '(GMT+05:00) Ashgabat', value: 'Asia/Ashgabat' },\n        { text: '(GMT+05:00) Dushanbe', value: 'Asia/Dushanbe' },\n        { text: '(GMT+05:00) Karachi', value: 'Asia/Karachi' },\n        { text: '(GMT+05:00) Kerguelen', value: 'Indian/Kerguelen' },\n        { text: '(GMT+05:00) Maldives', value: 'Indian/Maldives' },\n        { text: '(GMT+05:00) Mawson', value: 'Antarctica/Mawson' },\n        { text: '(GMT+05:00) Moscow+02 - Yekaterinburg', value: 'Asia/Yekaterinburg' },\n        { text: '(GMT+05:00) Tashkent', value: 'Asia/Tashkent' },\n        { text: '(GMT+05:30) Colombo', value: 'Asia/Colombo' },\n        { text: '(GMT+05:30) India Standard Time', value: 'Asia/Kolkata' },\n        { text: '(GMT+05:45) Kathmandu', value: 'Asia/Kathmandu' },\n        { text: '(GMT+06:00) Almaty', value: 'Asia/Almaty' },\n        { text: '(GMT+06:00) Bishkek', value: 'Asia/Bishkek' },\n        { text: '(GMT+06:00) Chagos', value: 'Indian/Chagos' },\n        { text: '(GMT+06:00) Dhaka', value: 'Asia/Dhaka' },\n        { text: '(GMT+06:00) Moscow+03 - Omsk', value: 'Asia/Omsk' },\n        { text: '(GMT+06:00) Thimphu', value: 'Asia/Thimphu' },\n        { text: '(GMT+06:00) Vostok', value: 'Antarctica/Vostok' },\n        { text: '(GMT+06:30) Cocos', value: 'Indian/Cocos' },\n        { text: '(GMT+06:30) Rangoon', value: 'Asia/Yangon' },\n        { text: '(GMT+07:00) Bangkok', value: 'Asia/Bangkok' },\n        { text: '(GMT+07:00) Christmas', value: 'Indian/Christmas' },\n        { text: '(GMT+07:00) Davis', value: 'Antarctica/Davis' },\n        { text: '(GMT+07:00) Hanoi', value: 'Asia/Saigon' },\n        { text: '(GMT+07:00) Hovd', value: 'Asia/Hovd' },\n        { text: '(GMT+07:00) Jakarta', value: 'Asia/Jakarta' },\n        { text: '(GMT+07:00) Moscow+04 - Krasnoyarsk', value: 'Asia/Krasnoyarsk' },\n        { text: '(GMT+08:00) Brunei', value: 'Asia/Brunei' },\n        { text: '(GMT+08:00) China Time - Beijing', value: 'Asia/Shanghai' },\n        { text: '(GMT+08:00) Choibalsan', value: 'Asia/Choibalsan' },\n        { text: '(GMT+08:00) Hong Kong', value: 'Asia/Hong_Kong' },\n        { text: '(GMT+08:00) Kuala Lumpur', value: 'Asia/Kuala_Lumpur' },\n        { text: '(GMT+08:00) Macau', value: 'Asia/Macau' },\n        { text: '(GMT+08:00) Makassar', value: 'Asia/Makassar' },\n        { text: '(GMT+08:00) Manila', value: 'Asia/Manila' },\n        { text: '(GMT+08:00) Moscow+05 - Irkutsk', value: 'Asia/Irkutsk' },\n        { text: '(GMT+08:00) Singapore', value: 'Asia/Singapore' },\n        { text: '(GMT+08:00) Taipei', value: 'Asia/Taipei' },\n        { text: '(GMT+08:00) Ulaanbaatar', value: 'Asia/Ulaanbaatar' },\n        { text: '(GMT+08:00) Western Time - Perth', value: 'Australia/Perth' },\n        { text: '(GMT+08:30) Pyongyang', value: 'Asia/Pyongyang' },\n        { text: '(GMT+09:00) Dili', value: 'Asia/Dili' },\n        { text: '(GMT+09:00) Jayapura', value: 'Asia/Jayapura' },\n        { text: '(GMT+09:00) Moscow+06 - Yakutsk', value: 'Asia/Yakutsk' },\n        { text: '(GMT+09:00) Palau', value: 'Pacific/Palau' },\n        { text: '(GMT+09:00) Seoul', value: 'Asia/Seoul' },\n        { text: '(GMT+09:00) Tokyo', value: 'Asia/Tokyo' },\n        { text: '(GMT+09:30) Central Time - Darwin', value: 'Australia/Darwin' },\n        { text: '(GMT+10:00) Dumont D\\'Urville', value: 'Antarctica/DumontDUrville' },\n        { text: '(GMT+10:00) Eastern Time - Brisbane', value: 'Australia/Brisbane' },\n        { text: '(GMT+10:00) Guam', value: 'Pacific/Guam' },\n        { text: '(GMT+10:00) Moscow+07 - Vladivostok', value: 'Asia/Vladivostok' },\n        { text: '(GMT+10:00) Port Moresby', value: 'Pacific/Port_Moresby' },\n        { text: '(GMT+10:00) Truk', value: 'Pacific/Chuuk' },\n        { text: '(GMT+10:30) Central Time - Adelaide', value: 'Australia/Adelaide' },\n        { text: '(GMT+11:00) Casey', value: 'Antarctica/Casey' },\n        { text: '(GMT+11:00) Eastern Time - Hobart', value: 'Australia/Hobart' },\n        { text: '(GMT+11:00) Eastern Time - Melbourne, Sydney', value: 'Australia/Sydney' },\n        { text: '(GMT+11:00) Efate', value: 'Pacific/Efate' },\n        { text: '(GMT+11:00) Guadalcanal', value: 'Pacific/Guadalcanal' },\n        { text: '(GMT+11:00) Kosrae', value: 'Pacific/Kosrae' },\n        { text: '(GMT+11:00) Moscow+08 - Magadan', value: 'Asia/Magadan' },\n        { text: '(GMT+11:00) Norfolk', value: 'Pacific/Norfolk' },\n        { text: '(GMT+11:00) Noumea', value: 'Pacific/Noumea' },\n        { text: '(GMT+11:00) Ponape', value: 'Pacific/Pohnpei' },\n        { text: '(GMT+12:00) Funafuti', value: 'Pacific/Funafuti' },\n        { text: '(GMT+12:00) Kwajalein', value: 'Pacific/Kwajalein' },\n        { text: '(GMT+12:00) Majuro', value: 'Pacific/Majuro' },\n        { text: '(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy', value: 'Asia/Kamchatka' },\n        { text: '(GMT+12:00) Nauru', value: 'Pacific/Nauru' },\n        { text: '(GMT+12:00) Tarawa', value: 'Pacific/Tarawa' },\n        { text: '(GMT+12:00) Wake', value: 'Pacific/Wake' },\n        { text: '(GMT+12:00) Wallis', value: 'Pacific/Wallis' },\n        { text: '(GMT+13:00) Auckland', value: 'Pacific/Auckland' },\n        { text: '(GMT+13:00) Enderbury', value: 'Pacific/Enderbury' },\n        { text: '(GMT+13:00) Fakaofo', value: 'Pacific/Fakaofo' },\n        { text: '(GMT+13:00) Fiji', value: 'Pacific/Fiji' },\n        { text: '(GMT+13:00) Tongatapu', value: 'Pacific/Tongatapu' },\n        { text: '(GMT+14:00) Apia', value: 'Pacific/Apia' },\n        { text: '(GMT+14:00) Kiritimati', value: 'Pacific/Kiritimati' }\n      ]\n    }\n  },\n  computed: {\n    dateFormats () {\n      return [\n        { text: this.$t('profile:localeDefault'), value: '' },\n        { text: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },\n        { text: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },\n        { text: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },\n        { text: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },\n        { text: 'YYYY/MM/DD', value: 'YYYY/MM/DD' }\n      ]\n    },\n    appearances () {\n      return [\n        { text: this.$t('profile:appearanceDefault'), value: '' },\n        { text: this.$t('profile:appearanceLight'), value: 'light' },\n        { text: this.$t('profile:appearanceDark'), value: 'dark' }\n      ]\n    },\n    currentAppearance () {\n      return _.get(_.find(this.appearances, ['value', this.user.appearance]), 'text', false) || this.$t('profile:appearanceDefault')\n    },\n    pictureUrl: get('user/pictureUrl'),\n    picture () {\n      if (this.pictureUrl && this.pictureUrl.length > 1) {\n        return {\n          kind: 'image',\n          url: this.pictureUrl\n        }\n      } else {\n        const nameParts = this.user.name.toUpperCase().split(' ')\n        let initials = _.head(nameParts).charAt(0)\n        if (nameParts.length > 1) {\n          initials += _.last(nameParts).charAt(0)\n        }\n        return {\n          kind: 'initials',\n          initials\n        }\n      }\n    }\n  },\n  watch: {\n    'user.appearance': (newValue, oldValue) => {\n      if (newValue === '') {\n        WIKI.$vuetify.theme.dark = siteConfig.darkMode\n      } else {\n        WIKI.$vuetify.theme.dark = (newValue === 'dark')\n      }\n    },\n    'user.dateFormat': (newValue, oldValue) => {\n      if (newValue === '') {\n        WIKI.$moment.updateLocale(WIKI.$moment.locale(), null)\n      } else {\n        WIKI.$moment.updateLocale(WIKI.$moment.locale(), {\n          longDateFormat: {\n            'L': newValue\n          }\n        })\n      }\n    },\n    'user.timezone': (newValue, oldValue) => {\n      if (newValue === '') {\n        WIKI.$moment.tz.setDefault()\n      } else {\n        WIKI.$moment.tz.setDefault(newValue)\n      }\n    }\n  },\n  methods: {\n    /**\n     * Focus an input after delay\n     */\n    focusField (ipt) {\n      this.$nextTick(() => {\n        _.delay(() => {\n          this.$refs[ipt].focus()\n        }, 200)\n      })\n    },\n    /**\n     * Save User Profile\n     */\n    async saveProfile () {\n      this.saveLoading = true\n      this.$store.commit(`loadingStart`, 'profile-save')\n\n      try {\n        const respRaw = await this.$apollo.mutate({\n          mutation: gql`\n            mutation ($name: String!, $location: String!, $jobTitle: String!, $timezone: String!, $dateFormat: String!, $appearance: String!) {\n              users {\n                updateProfile(name: $name, location: $location, jobTitle: $jobTitle, timezone: $timezone, dateFormat: $dateFormat, appearance: $appearance) {\n                  responseResult {\n                    succeeded\n                    errorCode\n                    slug\n                    message\n                  }\n                  jwt\n                }\n              }\n            }\n          `,\n          variables: {\n            name: this.user.name,\n            location: this.user.location,\n            jobTitle: this.user.jobTitle,\n            timezone: this.user.timezone,\n            dateFormat: this.user.dateFormat,\n            appearance: this.user.appearance\n          }\n        })\n        const resp = _.get(respRaw, 'data.users.updateProfile.responseResult', {})\n        if (resp.succeeded) {\n          Cookies.set('jwt', _.get(respRaw, 'data.users.updateProfile.jwt', ''), { expires: 365, secure: window.location.protocol === 'https:' })\n          this.$store.set('user/name', this.user.name)\n          this.$store.commit('showNotification', {\n            message: this.$t('profile:save.success'),\n            style: 'success',\n            icon: 'check'\n          })\n        } else {\n          throw new Error(resp.message)\n        }\n      } catch (err) {\n        this.$store.commit('pushGraphError', err)\n      }\n\n      this.$store.commit(`loadingStop`, 'profile-save')\n      this.saveLoading = false\n    },\n    /**\n     * Change Password\n     */\n    async changePassword () {\n      const validation = validate({\n        current: this.currentPass,\n        password: this.newPass,\n        verifyPassword: this.verifyPass\n      }, {\n        current: {\n          presence: {\n            message: this.$t('auth:missingPassword'),\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6,\n            tooShort: this.$t('auth:passwordTooShort')\n          }\n        },\n        password: {\n          presence: {\n            message: this.$t('auth:missingPassword'),\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6,\n            tooShort: this.$t('auth:passwordTooShort')\n          }\n        },\n        verifyPassword: {\n          equality: {\n            attribute: 'password',\n            message: this.$t('auth:passwordNotMatch')\n          }\n        }\n      }, { fullMessages: false })\n\n      if (validation) {\n        if (validation.current) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.current[0],\n            icon: 'warning'\n          })\n          this.$refs.iptCurrentPass.focus()\n        } else if (validation.password) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.password[0],\n            icon: 'warning'\n          })\n          this.$refs.iptNewPass.focus()\n        } else if (validation.verifyPassword) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.verifyPassword[0],\n            icon: 'warning'\n          })\n          this.$refs.iptVerifyPass.focus()\n        }\n      } else {\n        this.changePassLoading = true\n        this.$store.commit(`loadingStart`, 'profile-changepassword')\n\n        try {\n          const respRaw = await this.$apollo.mutate({\n            mutation: gql`\n              mutation ($current: String!, $new: String!) {\n                users {\n                  changePassword(current: $current, new: $new) {\n                    responseResult {\n                      succeeded\n                      errorCode\n                      slug\n                      message\n                    }\n                    jwt\n                  }\n                }\n              }\n            `,\n            variables: {\n              current: this.currentPass,\n              new: this.newPass\n            }\n          })\n          const resp = _.get(respRaw, 'data.users.changePassword.responseResult', {})\n          if (resp.succeeded) {\n            this.currentPass = ''\n            this.newPass = ''\n            this.verifyPass = ''\n            Cookies.set('jwt', _.get(respRaw, 'data.users.changePassword.jwt', ''), { expires: 365, secure: window.location.protocol === 'https:' })\n            this.$store.commit('showNotification', {\n              message: this.$t('profile:auth.changePassSuccess'),\n              style: 'success',\n              icon: 'check'\n            })\n          } else {\n            throw new Error(resp.message)\n          }\n        } catch (err) {\n          this.$store.commit('pushGraphError', err)\n        }\n\n        this.$store.commit(`loadingStop`, 'profile-changepassword')\n        this.changePassLoading = false\n      }\n    }\n  },\n  apollo: {\n    user: {\n      query: gql`\n        {\n          users {\n            profile {\n              id\n              name\n              email\n              providerKey\n              providerName\n              isSystem\n              isVerified\n              location\n              jobTitle\n              timezone\n              dateFormat\n              appearance\n              createdAt\n              updatedAt\n              lastLoginAt\n              groups\n              pagesTotal\n            }\n          }\n        }\n      `,\n      fetchPolicy: 'network-only',\n      update: (data) => _.cloneDeep(data.users.profile),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'profile-refresh')\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/profile.vue",
    "content": "<template lang='pug'>\n  v-app(:dark='$vuetify.theme.dark').profile\n    nav-header\n    v-navigation-drawer.pb-0(v-model='profileDrawerShown', app, fixed, clipped, left, permanent)\n      v-list(dense, nav)\n        v-list-item(to='/profile', color='primary')\n          v-list-item-action: v-icon mdi-face-profile\n          v-list-item-content\n            v-list-item-title {{$t('profile:title')}}\n        //- v-list-item(to='/preferences', disabled)\n        //-   v-list-item-action: v-icon(color='grey lighten-1') mdi-cog-outline\n        //-   v-list-item-content\n        //-     v-list-item-title Preferences\n        //-     v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon\n        v-list-item(to='/pages', color='primary')\n          v-list-item-action: v-icon mdi-file-document-outline\n          v-list-item-content\n            v-list-item-title {{$t('profile:pages.title')}}\n        //- v-list-item(to='/comments', disabled)\n        //-   v-list-item-action: v-icon(color='grey lighten-1') mdi-message-reply-text\n        //-   v-list-item-content\n        //-     v-list-item-title {{$t('profile:comments.title')}}\n        //-     v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon\n\n    v-content(:class='$vuetify.theme.dark ? \"grey darken-4\" : \"grey lighten-5\"')\n      transition(name='profile-router')\n        router-view\n\n    nav-footer\n    notify\n    search-results\n</template>\n\n<script>\nimport VueRouter from 'vue-router'\n\n/* global WIKI */\n\nconst router = new VueRouter({\n  mode: 'history',\n  base: '/p',\n  routes: [\n    { path: '/', redirect: '/profile' },\n    { path: '/profile', component: () => import(/* webpackChunkName: \"profile\" */ './profile/profile.vue') },\n    { path: '/pages', component: () => import(/* webpackChunkName: \"profile\" */ './profile/pages.vue') },\n    { path: '/comments', component: () => import(/* webpackChunkName: \"profile\" */ './profile/comments.vue') }\n  ]\n})\n\nrouter.beforeEach((to, from, next) => {\n  WIKI.$store.commit('loadingStart', 'profile')\n  next()\n})\n\nrouter.afterEach((to, from) => {\n  WIKI.$store.commit('loadingStop', 'profile')\n})\n\nexport default {\n  i18nOptions: { namespaces: 'profile' },\n  data() {\n    return {\n      profileDrawerShown: true\n    }\n  },\n  router,\n  created() {\n    this.$store.commit('page/SET_MODE', 'profile')\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.profile-router {\n  &-enter-active, &-leave-active {\n    transition: opacity .25s ease;\n    opacity: 1;\n  }\n  &-enter-active {\n    transition-delay: .25s;\n  }\n  &-enter, &-leave-to {\n    opacity: 0;\n  }\n}\n\n.profile-header {\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n\n  &-title {\n    margin-left: 1rem;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/register.vue",
    "content": "<template lang=\"pug\">\n  v-app\n    .register\n      v-container(grid-list-lg)\n        v-layout(row, wrap)\n          v-flex(\n            xs12\n            offset-sm1, sm10\n            offset-md2, md8\n            offset-lg3, lg6\n            offset-xl4, xl4\n            )\n            transition(name='fadeUp')\n              v-card.elevation-5.md2(v-show='isShown')\n                v-toolbar(color='indigo', flat, dense, dark)\n                  v-spacer\n                  .subheading {{ $t('auth:registerTitle') }}\n                  v-spacer\n                v-card-text.text-center\n                  h1.display-1.indigo--text.py-2 {{ siteTitle }}\n                  .body-2 {{ $t('auth:registerSubTitle') }}\n                  v-text-field.md2.mt-3(\n                    solo\n                    flat\n                    prepend-icon='mdi-email'\n                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'\n                    hide-details\n                    ref='iptEmail'\n                    v-model='email'\n                    :placeholder='$t(\"auth:fields.email\")'\n                    color='indigo'\n                    )\n                  v-text-field.md2.mt-2(\n                    solo\n                    flat\n                    prepend-icon='mdi-form-textbox-password'\n                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'\n                    ref='iptPassword'\n                    v-model='password'\n                    :append-icon='hidePassword ? \"mdi-eye-off\" : \"mdi-eye\"'\n                    @click:append='() => (hidePassword = !hidePassword)'\n                    :type='hidePassword ? \"password\" : \"text\"'\n                    :placeholder='$t(\"auth:fields.password\")'\n                    color='indigo'\n                    loading\n                    counter='255'\n                    )\n                    password-strength(slot='progress', v-model='password')\n                  v-text-field.md2.mt-2(\n                    solo\n                    flat\n                    prepend-icon='mdi-form-textbox-password'\n                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'\n                    hide-details\n                    ref='iptVerifyPassword'\n                    v-model='verifyPassword'\n                    @click:append='() => (hidePassword = !hidePassword)'\n                    type='password'\n                    :placeholder='$t(\"auth:fields.verifyPassword\")'\n                    color='indigo'\n                  )\n                  v-text-field.md2.mt-2(\n                    solo\n                    flat\n                    prepend-icon='mdi-account'\n                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'\n                    ref='iptName'\n                    v-model='name'\n                    :placeholder='$t(\"auth:fields.name\")'\n                    @keyup.enter='register'\n                    color='indigo'\n                    counter='255'\n                    )\n                v-card-actions.pb-4\n                  v-spacer\n                  v-btn.md2(\n                    width='100%'\n                    max-width='250px'\n                    large\n                    dark\n                    color='indigo'\n                    @click='register'\n                    rounded\n                    :loading='isLoading'\n                    ) {{ $t('auth:actions.register') }}\n                  v-spacer\n                v-divider\n                v-card-actions.py-3.grey(:class='$vuetify.theme.dark ? `darken-4-l1` : `lighten-4`')\n                  v-spacer\n                  i18next.caption(path='auth:switchToLogin.text', tag='div')\n                    a.caption(href='/login', place='link') {{ $t('auth:switchToLogin.link') }}\n                  v-spacer\n\n    loader(v-model='isLoading', :mode='loaderMode', :icon='loaderIcon', :color='loaderColor', :title='loaderTitle', :subtitle='loaderSubtitle')\n    nav-footer(color='grey darken-5', dark-color='grey darken-5')\n    notify(style='padding-top: 64px;')\n</template>\n\n<script>\n/* global siteConfig */\n\nimport _ from 'lodash'\nimport validate from 'validate.js'\nimport PasswordStrength from './common/password-strength.vue'\n\nimport registerMutation from 'gql/register/register-mutation-create.gql'\n\nexport default {\n  i18nOptions: { namespaces: 'auth' },\n  components: {\n    PasswordStrength\n  },\n  data () {\n    return {\n      email: '',\n      password: '',\n      verifyPassword: '',\n      name: '',\n      hidePassword: true,\n      isLoading: false,\n      isShown: false,\n      loaderColor: 'grey darken-4',\n      loaderTitle: 'Working...',\n      loaderSubtitle: 'Please wait',\n      loaderMode: 'icon',\n      loaderIcon: 'checkmark'\n    }\n  },\n  computed: {\n    siteTitle () {\n      return siteConfig.title\n    }\n  },\n  mounted () {\n    this.isShown = true\n    this.$nextTick(() => {\n      this.$refs.iptEmail.focus()\n    })\n  },\n  methods: {\n    /**\n     * REGISTER\n     */\n    async register () {\n      const validation = validate({\n        email: this.email,\n        password: this.password,\n        verifyPassword: this.verifyPassword,\n        name: this.name\n      }, {\n        email: {\n          presence: {\n            message: this.$t('auth:missingEmail'),\n            allowEmpty: false\n          },\n          email: {\n            message: this.$t('auth:invalidEmail')\n          }\n        },\n        password: {\n          presence: {\n            message: this.$t('auth:missingPassword'),\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6,\n            tooShort: this.$t('auth:passwordTooShort')\n          }\n        },\n        verifyPassword: {\n          equality: {\n            attribute: 'password',\n            message: this.$t('auth:passwordNotMatch')\n          }\n        },\n        name: {\n          presence: {\n            message: this.$t('auth:missingName'),\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255,\n            tooShort: this.$t('auth:nameTooShort'),\n            tooLong: this.$t('auth:nameTooLong')\n          }\n        }\n      }, { fullMessages: false })\n\n      if (validation) {\n        if (validation.email) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.email[0],\n            icon: 'warning'\n          })\n          this.$refs.iptEmail.focus()\n        } else if (validation.password) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.password[0],\n            icon: 'warning'\n          })\n          this.$refs.iptPassword.focus()\n        } else if (validation.verifyPassword) {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.verifyPassword[0],\n            icon: 'warning'\n          })\n          this.$refs.iptVerifyPassword.focus()\n        } else {\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: validation.name[0],\n            icon: 'warning'\n          })\n          this.$refs.iptName.focus()\n        }\n      } else {\n        this.loaderColor = 'grey darken-4'\n        this.loaderTitle = this.$t('auth:registering')\n        this.loaderSubtitle = this.$t(`auth:pleaseWait`)\n        this.loaderMode = 'loading'\n        this.isLoading = true\n        try {\n          let resp = await this.$apollo.mutate({\n            mutation: registerMutation,\n            variables: {\n              email: this.email,\n              password: this.password,\n              name: this.name\n            }\n          })\n          if (_.has(resp, 'data.authentication.register')) {\n            let respObj = _.get(resp, 'data.authentication.register', {})\n            if (respObj.responseResult.succeeded === true) {\n              this.loaderColor = 'grey darken-4'\n              this.loaderTitle = this.$t('auth:registerSuccess')\n              this.loaderSubtitle = this.$t(`auth:registerCheckEmail`)\n              this.loaderMode = 'icon'\n              this.isShown = false\n            } else {\n              throw new Error(respObj.responseResult.message)\n            }\n          } else {\n            throw new Error(this.$t('auth:genericError'))\n          }\n        } catch (err) {\n          console.error(err)\n          this.$store.commit('showNotification', {\n            style: 'red',\n            message: err.message,\n            icon: 'warning'\n          })\n          this.isLoading = false\n        }\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n  .register {\n    background-color: mc('indigo', '900');\n    background-image: url('../static/svg/motif-blocks.svg');\n    background-repeat: repeat;\n    background-size: 200px;\n    width: 100%;\n    height: 100%;\n    animation: loginBgReveal 20s linear infinite;\n\n    @include keyframes(loginBgReveal) {\n      0% {\n        background-position-x: 0;\n      }\n      100% {\n        background-position-x: 800px;\n      }\n    }\n\n    &::before {\n      content: '';\n      position: absolute;\n      background-image: url('../static/svg/motif-overlay.svg');\n      background-attachment: fixed;\n      background-size: cover;\n      opacity: .5;\n      top: 0;\n      left: 0;\n      width: 100vw;\n      height: 100vh;\n    }\n\n    > .container {\n      height: 100%;\n      align-items: center;\n      display: flex;\n    }\n\n    .v-text-field.centered input {\n      text-align: center;\n    }\n  }\n</style>\n"
  },
  {
    "path": "client/components/setup.vue",
    "content": "<template lang=\"pug\">\n  v-app.setup\n    v-content\n      v-container\n        v-layout\n          v-flex(xs12, lg6, offset-lg3)\n            v-card.elevation-20.radius-7.animated.fadeInUp\n              v-alert(v-if='isDevMode', tile, dark, color='red darken-3', icon='mdi-alert', prominent)\n                .body-2 You are running an unstable, unreleased development version. This code base is #[strong NOT] for production use!\n                .body-2.mt-3 Cloning the dev branch directly from GitHub is #[strong NOT] the proper way to install Wiki.js!\n                .body-2 Read the #[a(href='https://docs.requarks.io/install', style='color: #FFF;') documentation] on correctly installing the latest stable version.\n              .text-center\n                img.setup-logo.animated.fadeInUp.wait-p2s(src='/_assets/svg/logo-wikijs-full.svg', alt='Wiki.js Logo')\n              v-alert(v-model='error', type='error', icon='mdi-alert', tile, dismissible) {{ errorMessage }}\n              v-alert(v-if='!error', tile, color='blue lighten-5', :value='true')\n                v-icon.mr-3(color='blue') mdi-package-variant\n                span.blue--text You are about to install Wiki.js #[strong {{wikiVersion}}].\n              v-card-text\n                .overline.pl-3 Administrator Account\n                v-container.pa-3.mt-3(grid-list-xl)\n                  v-layout(row, wrap)\n                    v-flex(xs12)\n                      v-text-field(\n                        outlined\n                        v-model='conf.adminEmail',\n                        label='Administrator Email',\n                        hint='The email address of the administrator account.',\n                        persistent-hint\n                        required\n                        ref='adminEmailInput'\n                      )\n                    v-flex(xs6)\n                      v-text-field(\n                        outlined\n                        ref='adminPassword',\n                        counter='255'\n                        v-model='conf.adminPassword',\n                        label='Password',\n                        :append-icon=\"pwdMode ? 'mdi-eye-off' : 'mdi-eye'\"\n                        @click:append=\"() => (pwdMode = !pwdMode)\"\n                        :type=\"pwdMode ? 'password' : 'text'\"\n                        hint='At least 8 characters long.',\n                        persistent-hint\n                      )\n                    v-flex(xs6)\n                      v-text-field(\n                        outlined\n                        ref='adminPasswordConfirm',\n                        counter='255'\n                        v-model='conf.adminPasswordConfirm',\n                        label='Confirm Password',\n                        :append-icon=\"pwdConfirmMode ? 'mdi-eye-off' : 'mdi-eye'\"\n                        @click:append=\"() => (pwdConfirmMode = !pwdConfirmMode)\"\n                        :type=\"pwdConfirmMode ? 'password' : 'text'\"\n                        hint='Verify your password again.',\n                        persistent-hint\n                      )\n                v-divider.mb-4\n                .overline.pl-3.mb-5 Site URL\n                v-text-field.mb-4.mx-3(\n                  outlined\n                  ref='adminSiteUrl',\n                  v-model='conf.siteUrl',\n                  label='Site URL',\n                  hint='Full URL to your wiki, without the trailing slash (e.g. https://wiki.example.com). This should be the public facing URL, not the internal one if using a reverse-proxy.',\n                  persistent-hint\n                  @keyup.enter='install'\n                )\n                v-divider.mb-4\n                .overline.pl-3.mb-3 Telemetry\n                v-switch.ml-3(\n                  inset\n                  color='primary',\n                  v-model='conf.telemetry',\n                  label='Allow Telemetry',\n                  persistent-hint,\n                  hint='Help Wiki.js developers improve this app with anonymized telemetry.'\n                )\n                a.pl-3(style='font-size: 12px; letter-spacing: initial;', href='https://docs.requarks.io/telemetry', target='_blank') Learn more\n              v-divider.mt-2\n              v-card-actions\n                v-btn(color='primary', @click='install', :disabled='loading', x-large, depressed, block)\n                  v-icon(left) mdi-check\n                  span Install\n\n    v-dialog(v-model='loading', width='450', persistent)\n      v-card(color='primary', dark).radius-7\n        v-card-text.text-center.py-5\n          .py-3(style='width: 64px; display:inline-block;')\n            breeding-rhombus-spinner(\n              :animation-duration='2000'\n              :size='64'\n              color='#FFF'\n              )\n          template(v-if='!success')\n            .subtitle-1.white--text Finalizing your installation...\n            .caption Just a moment\n          template(v-else)\n            .subtitle-1.white--text Installation complete!\n            .caption Redirecting...\n</template>\n\n<script>\nimport _ from 'lodash'\nimport validate from 'validate.js'\nimport { BreedingRhombusSpinner } from 'epic-spinners'\nimport confetti from 'canvas-confetti'\n\n/* global siteConfig */\n\nexport default {\n  components: {\n    BreedingRhombusSpinner\n  },\n  props: {\n    wikiVersion: {\n      type: String,\n      required: true\n    }\n  },\n  data() {\n    return {\n      loading: false,\n      success: false,\n      error: false,\n      errorMessage: '',\n      conf: {\n        adminEmail: '',\n        adminPassword: '',\n        adminPasswordConfirm: '',\n        siteUrl: 'https://wiki.yourdomain.com',\n        telemetry: true\n      },\n      pwdMode: true,\n      pwdConfirmMode: true,\n      isDevMode: false\n    }\n  },\n  mounted() {\n    _.delay(() => {\n      this.$refs.adminEmailInput.focus()\n    }, 500)\n    this.isDevMode = siteConfig.devMode === true\n  },\n  methods: {\n    async install () {\n      this.error = false\n\n      const validationResults = validate(this.conf, {\n        adminEmail: {\n          presence: {\n            allowEmpty: false\n          },\n          email: true\n        },\n        adminPassword: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6,\n            maximum: 255\n          }\n        },\n        adminPasswordConfirm: {\n          equality: 'adminPassword'\n        },\n        siteUrl: {\n          presence: {\n            allowEmpty: false\n          },\n          url: {\n            schemes: ['http', 'https'],\n            allowLocal: true,\n            allowDataUrl: false\n          },\n          format: {\n            pattern: '^(?!.*/$).*$',\n            flags: 'i',\n            message: 'must not have a trailing slash'\n          }\n        }\n      }, {\n        format: 'flat'\n      })\n      if (validationResults) {\n        this.error = true\n        this.errorMessage = validationResults[0]\n        this.$forceUpdate()\n        return\n      }\n\n      this.loading = true\n      this.success = false\n      this.$forceUpdate()\n\n      _.delay(async () => {\n        try {\n          const resp = await fetch('/finalize', {\n            method: 'POST',\n            cache: 'no-cache',\n            headers: {\n              'Content-Type': 'application/json'\n            },\n            body: JSON.stringify(this.conf)\n          }).then(res => res.json())\n\n          if (resp.ok === true) {\n            _.delay(() => {\n              confetti({\n                particleCount: 100,\n                spread: 70,\n                zIndex: 100000\n              })\n              this.success = true\n              _.delay(() => {\n                window.location.assign('/login')\n              }, 3000)\n            }, 10000)\n          } else {\n            this.error = true\n            this.errorMessage = resp.error\n            this.loading = false\n          }\n        } catch (err) {\n          window.alert(err.message)\n        }\n      }, 1000)\n    }\n  }\n}\n\n</script>\n\n<style lang='scss'>\n.setup {\n  .v-application--wrap {\n    padding-top: 10vh;\n    background-color: #111;\n    background-image: linear-gradient(45deg, mc('blue', '100'), mc('blue', '700'), mc('indigo', '900'));\n    background-blend-mode: exclusion;\n\n    &::before {\n      content: '';\n      position: absolute;\n      left: 0;\n      top: 0;\n      width: 100%;\n      height: 100vh;\n      z-index: 0;\n      background-color: transparent;\n      background-image: url(../static/svg/motif-grid.svg) !important;\n      background-size: 100px;\n      background-repeat: repeat;\n      animation: bg-anim 100s linear infinite;\n    }\n  }\n\n  @keyframes bg-anim {\n    0% {\n      background-position: 0 0;\n    }\n    100% {\n      background-position: 100% 100%;\n    }\n  }\n\n  &-logo {\n    width: 400px;\n    margin: 2rem 0 2rem 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/source.vue",
    "content": "<template lang='pug'>\n  v-app(:dark='$vuetify.theme.dark').source\n    nav-header\n    v-content\n      v-toolbar(color='primary', dark)\n        i18next.subheading(v-if='versionId > 0', path='common:page.viewingSourceVersion', tag='div')\n          strong(place='date', :title='$options.filters.moment(versionDate, `LLL`)') {{versionDate | moment('lll')}}\n          strong(place='path') /{{path}}\n        i18next.subheading(v-else, path='common:page.viewingSource', tag='div')\n          strong(place='path') /{{path}}\n        template(v-if='$vuetify.breakpoint.mdAndUp')\n          v-spacer\n          .caption.blue--text.text--lighten-3 {{$t('common:page.id', { id: pageId })}}\n          .caption.blue--text.text--lighten-3.ml-4(v-if='versionId > 0') {{$t('common:page.versionId', { id: versionId })}}\n          v-btn.ml-4(v-if='versionId > 0', depressed, color='blue darken-1', @click='goHistory')\n            v-icon mdi-history\n          v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') {{$t('common:page.returnNormalView')}}\n      v-card(tile)\n        v-card-text\n          v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-4`')\n            v-card-text\n              pre\n                slot\n\n    nav-footer\n    notify\n    search-results\n</template>\n\n<script>\nexport default {\n  props: {\n    pageId: {\n      type: Number,\n      default: 0\n    },\n    locale: {\n      type: String,\n      default: 'en'\n    },\n    path: {\n      type: String,\n      default: 'home'\n    },\n    versionId: {\n      type: Number,\n      default: 0\n    },\n    versionDate: {\n      type: String,\n      default: ''\n    },\n    effectivePermissions: {\n      type: String,\n      default: ''\n    }\n  },\n  data() {\n    return {}\n  },\n  created () {\n    this.$store.commit('page/SET_ID', this.id)\n    this.$store.commit('page/SET_LOCALE', this.locale)\n    this.$store.commit('page/SET_PATH', this.path)\n\n    this.$store.commit('page/SET_MODE', 'source')\n\n    if (this.effectivePermissions) {\n      this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))\n    }\n  },\n  methods: {\n    goLive() {\n      window.location.assign(`/${this.locale}/${this.path}`)\n    },\n    goHistory () {\n      window.location.assign(`/h/${this.locale}/${this.path}`)\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n.source {\n  pre > code {\n    box-shadow: none;\n    background-color: transparent;\n    color: mc('grey', '800');\n    font-family: 'Roboto Mono', sans-serif;\n    font-weight: 400;\n    font-size: 1rem;\n\n    @at-root .theme--dark.source pre > code {\n      background-color: mc('grey', '900');\n      color: mc('grey', '400');\n    }\n\n    &::before {\n      display: none;\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/components/tags.vue",
    "content": "<template lang='pug'>\n  v-app(:dark='$vuetify.theme.dark').tags\n    nav-header\n    v-navigation-drawer.pb-0.elevation-1(app, fixed, clipped, :right='$vuetify.rtl', permanent, width='300')\n      vue-scroll(:ops='scrollStyle')\n        v-list(dense, nav)\n          v-list-item(href='/')\n            v-list-item-icon: v-icon mdi-home\n            v-list-item-title {{$t('common:header.home')}}\n          template(v-for='(tags, groupName) in tagsGrouped')\n            v-divider.my-2\n            v-subheader.pl-4(:key='`tagGroup-` + groupName') {{groupName}}\n            v-list-item(v-for='tag of tags', @click='toggleTag(tag.tag)', :key='`tag-` + tag.tag')\n              v-list-item-icon\n                v-icon(v-if='isSelected(tag.tag)', color='primary') mdi-checkbox-intermediate\n                v-icon(v-else) mdi-checkbox-blank-outline\n              v-list-item-title {{tag.title}}\n    v-content.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-3`')\n      v-toolbar(color='primary', dark, flat, height='58')\n        template(v-if='selection.length > 0')\n          .overline.mr-3.animated.fadeInLeft {{$t('tags:currentSelection')}}\n          v-chip.mr-3.primary--text(\n            v-for='tag of tagsSelected'\n            :key='`tagSelected-` + tag.tag'\n            color='white'\n            close\n            @click:close='toggleTag(tag.tag)'\n            ) {{tag.title}}\n          v-spacer\n          v-btn.animated.fadeIn(\n            small\n            outlined\n            color='blue lighten-4'\n            rounded\n            @click='selection = []'\n            )\n            v-icon(left) mdi-close\n            span {{$t('tags:clearSelection')}}\n        template(v-else)\n          v-icon.mr-3.animated.fadeInRight mdi-arrow-left\n          .overline.animated.fadeInRight {{$t('tags:selectOneMoreTags')}}\n      v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-l5` : `grey lighten-4`', flat, height='58')\n        v-text-field.tags-search(\n          v-model='innerSearch'\n          :label='$t(`tags:searchWithinResultsPlaceholder`)'\n          solo\n          hide-details\n          flat\n          rounded\n          single-line\n          height='40'\n          prepend-icon='mdi-text-box-search-outline'\n          append-icon='mdi-arrow-right'\n          clearable\n        )\n        template(v-if='locales.length > 1')\n          v-divider.mx-3(vertical)\n          .overline {{$t('tags:locale')}}\n          v-select.ml-2(\n            :items='locales'\n            v-model='locale'\n            :background-color='$vuetify.theme.dark ? `grey darken-3` : `white`'\n            hide-details\n            :label='$t(`tags:locale`)'\n            item-text='name'\n            item-value='code'\n            rounded\n            single-line\n            dense\n            height='40'\n            style='max-width: 170px;'\n          )\n        v-divider.mx-3(vertical)\n        .overline {{$t('tags:orderBy')}}\n        v-select.ml-2(\n          :items='orderByItems'\n          v-model='orderBy'\n          :background-color='$vuetify.theme.dark ? `grey darken-3` : `white`'\n          hide-details\n          :label='$t(`tags:orderBy`)'\n          rounded\n          single-line\n          dense\n          height='40'\n          style='max-width: 250px;'\n        )\n        v-btn-toggle.ml-2(v-model='orderByDirection', rounded, mandatory)\n          v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-up\n          v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-down\n      v-divider\n      .text-center.pt-10(v-if='selection.length < 1')\n        img(src='/_assets/svg/icon-price-tag.svg')\n        .subtitle-2.grey--text {{$t('tags:selectOneMoreTagsHint')}}\n      .px-5.py-2(v-else)\n        v-data-iterator(\n          :items='pages'\n          :items-per-page='4'\n          :search='innerSearch'\n          :loading='isLoading'\n          :options.sync='pagination'\n          @page-count='pageTotal = $event'\n          hide-default-footer\n          ref='dude'\n          )\n          template(v-slot:loading)\n            .text-center.pt-10\n              v-progress-circular(\n                indeterminate\n                color='primary'\n                size='96'\n                width='2'\n                )\n              .subtitle-2.grey--text.mt-5 {{$t('tags:retrievingResultsLoading')}}\n          template(v-slot:no-data)\n            .text-center.pt-10\n              img(src='/_assets/svg/icon-info.svg')\n              .subtitle-2.grey--text {{$t('tags:noResults')}}\n          template(v-slot:no-results)\n            .text-center.pt-10\n              img(src='/_assets/svg/icon-info.svg')\n              .subtitle-2.grey--text {{$t('tags:noResultsWithFilter')}}\n          template(v-slot:default='props')\n            v-row(align='stretch')\n              v-col(\n                v-for='item of props.items'\n                :key='`page-` + item.id'\n                cols='12'\n                lg='6'\n                )\n                v-card.radius-7(\n                  @click='goTo(item)'\n                  style='height:100%;'\n                  :class='$vuetify.theme.dark ? `grey darken-4` : ``'\n                  )\n                  v-card-text\n                    .d-flex.flex-row.align-center\n                      .body-1: strong.primary--text {{item.title}}\n                      v-spacer\n                      i18next.caption(tag='div', path='tags:pageLastUpdated')\n                        span(place='date') {{item.updatedAt | moment('from')}}\n                    .body-2.grey--text {{item.description || '---'}}\n                    v-divider.my-2\n                    .d-flex.flex-row.align-center\n                      v-chip(small, label, :color='$vuetify.theme.dark ? `grey darken-3-l5` : `grey lighten-4`').overline {{item.locale}}\n                      .caption.ml-1 / {{item.path}}\n        .text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')\n          v-pagination(v-model='pagination.page', :length='pageTotal')\n\n    nav-footer\n    notify\n    search-results\n</template>\n\n<script>\nimport VueRouter from 'vue-router'\nimport _ from 'lodash'\n\nimport tagsQuery from 'gql/common/common-pages-query-tags.gql'\nimport pagesQuery from 'gql/common/common-pages-query-list.gql'\n\n/* global siteLangs */\n\nconst router = new VueRouter({\n  mode: 'history',\n  base: '/t'\n})\n\nexport default {\n  i18nOptions: { namespaces: 'tags' },\n  data() {\n    return {\n      tags: [],\n      selection: [],\n      innerSearch: '',\n      locale: 'any',\n      locales: [],\n      orderBy: 'title',\n      orderByDirection: 0,\n      pagination: {\n        page: 1,\n        itemsPerPage: 12,\n        mustSort: true,\n        sortBy: ['title'],\n        sortDesc: [false]\n      },\n      pages: [],\n      pageTotal: 0,\n      isLoading: true,\n      scrollStyle: {\n        vuescroll: {},\n        scrollPanel: {\n          initialScrollY: 0,\n          initialScrollX: 0,\n          scrollingX: false,\n          easing: 'easeOutQuad',\n          speed: 1000,\n          verticalNativeBarPos: this.$vuetify.rtl ? `left` : `right`\n        },\n        rail: {\n          gutterOfEnds: '2px'\n        },\n        bar: {\n          onlyShowBarOnScroll: false,\n          background: '#CCC',\n          hoverStyle: {\n            background: '#999'\n          }\n        }\n      }\n    }\n  },\n  computed: {\n    tagsGrouped () {\n      return _.groupBy(this.tags, t => t.title.charAt(0).toUpperCase())\n    },\n    tagsSelected () {\n      return _.filter(this.tags, t => _.includes(this.selection, t.tag))\n    },\n    orderByItems () {\n      return [\n        { text: this.$t('tags:orderByField.creationDate'), value: 'createdAt' },\n        { text: this.$t('tags:orderByField.ID'), value: 'id' },\n        { text: this.$t('tags:orderByField.lastModified'), value: 'updatedAt' },\n        { text: this.$t('tags:orderByField.path'), value: 'path' },\n        { text: this.$t('tags:orderByField.title'), value: 'title' }\n      ]\n    }\n  },\n  watch: {\n    locale (newValue, oldValue) {\n      this.rebuildURL()\n    },\n    orderBy (newValue, oldValue) {\n      this.rebuildURL()\n      this.pagination.sortBy = [newValue]\n    },\n    orderByDirection (newValue, oldValue) {\n      this.rebuildURL()\n      this.pagination.sortDesc = [newValue === 1]\n    }\n  },\n  router,\n  created () {\n    this.$store.commit('page/SET_MODE', 'tags')\n    this.selection = _.compact(decodeURI(this.$route.path).split('/'))\n  },\n  mounted () {\n    this.locales = _.concat(\n      [{name: this.$t('tags:localeAny'), code: 'any'}],\n      (siteLangs.length > 0 ? siteLangs : [])\n    )\n    if (this.$route.query.lang) {\n      this.locale = this.$route.query.lang\n    }\n    if (this.$route.query.sort) {\n      this.orderBy = this.$route.query.sort.toLowerCase()\n      switch (this.orderBy) {\n        case 'updatedat':\n          this.orderBy = 'updatedAt'\n          break\n      }\n      this.pagination.sortBy = [this.orderBy]\n    }\n    if (this.$route.query.dir) {\n      this.orderByDirection = this.$route.query.dir === 'asc' ? 0 : 1\n      this.pagination.sortDesc = [this.orderByDirection === 1]\n    }\n  },\n  methods: {\n    toggleTag (tag) {\n      if (_.includes(this.selection, tag)) {\n        this.selection = _.without(this.selection, tag)\n      } else {\n        this.selection.push(tag)\n      }\n      this.rebuildURL()\n    },\n    isSelected (tag) {\n      return _.includes(this.selection, tag)\n    },\n    rebuildURL () {\n      let urlObj = {\n        path: '/' + this.selection.join('/')\n      }\n      if (this.locale !== `any`) {\n        _.set(urlObj, 'query.lang', this.locale)\n      }\n      if (this.orderBy !== `TITLE`) {\n        _.set(urlObj, 'query.sort', this.orderBy.toLowerCase())\n      }\n      if (this.orderByDirection !== 0) {\n        _.set(urlObj, 'query.dir', this.orderByDirection === 0 ? `asc` : `desc`)\n      }\n      this.$router.push(urlObj)\n    },\n    goTo (page) {\n      window.location.assign(`/${page.locale}/${page.path}`)\n    }\n  },\n  apollo: {\n    tags: {\n      query: tagsQuery,\n      fetchPolicy: 'cache-and-network',\n      update: (data) => _.cloneDeep(data.pages.tags),\n      watchLoading (isLoading) {\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'tags-refresh')\n      }\n    },\n    pages: {\n      query: pagesQuery,\n      fetchPolicy: 'cache-and-network',\n      update: (data) => _.cloneDeep(data.pages.list),\n      watchLoading (isLoading) {\n        this.isLoading = isLoading\n        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'pages-refresh')\n      },\n      variables () {\n        return {\n          locale: this.locale === 'any' ? null : this.locale,\n          tags: this.selection\n        }\n      },\n      skip () {\n        return this.selection.length < 1\n      }\n    }\n  }\n}\n</script>\n\n<style lang='scss'>\n.tags-search {\n  .v-input__control {\n    min-height: initial !important;\n  }\n  .v-input__prepend-outer {\n    margin-top: 8px !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "client/components/unauthorized.vue",
    "content": "<template lang='pug'>\n  v-app\n    .unauthorized\n      .unauthorized-content\n        img.animated.fadeIn(src='/_assets/svg/icon-delete-shield.svg', alt='Unauthorized')\n        .headline {{$t('unauthorized.title')}}\n        .subtitle-1.mt-3 {{$t('unauthorized.action.' + action)}}\n        v-btn.mt-5(href='/login', x-large)\n          v-icon(left) mdi-login\n          span {{$t('unauthorized.login')}}\n        v-btn.mt-5(color='red lighten-4', href='javascript:window.history.go(-1);', outlined)\n          v-icon(left) mdi-arrow-left\n          span {{$t('unauthorized.goback')}}\n</template>\n\n<script>\n\nexport default {\n  props: {\n    action: {\n      type: String,\n      default: 'view'\n    }\n  },\n  data() {\n    return { }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/components/welcome.vue",
    "content": "<template lang='pug'>\n  v-app\n    .onboarding\n      .onboarding-content\n        img.animated.fadeIn(src='/_assets/svg/logo-wikijs.svg', alt='Wiki.js')\n        .headline.animated.fadeInUp {{ $t('welcome.title') }}\n        .subtitle-1.mt-3.animated.fadeInUp.wait-p1s {{ $t('welcome.subtitle') }}\n        div\n          v-btn.mt-5.mx-3.animated.fadeInUp.wait-p2s(color='primary', :href='`/e/` + locale + `/home`', x-large)\n            v-icon(left) mdi-plus\n            span {{ $t('welcome.createhome') }}\n          v-btn.mt-5.mx-3.animated.fadeInUp.wait-p3s(color='primary', href='/a', x-large)\n            v-icon(left) mdi-view-dashboard\n            span {{ $t('welcome.goadmin') }}\n\n</template>\n\n<script>\n\nexport default {\n  props: {\n    locale: {\n      type: String,\n      default: 'en'\n    }\n  },\n  data() {\n    return { }\n  }\n}\n</script>\n\n<style lang='scss'>\n\n</style>\n"
  },
  {
    "path": "client/graph/admin/analytics/analytics-mutation-save-providers.gql",
    "content": "mutation($providers: [AnalyticsProviderInput]!) {\n  analytics {\n    updateProviders(providers: $providers) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/analytics/analytics-query-providers.gql",
    "content": "query {\n  analytics {\n    providers {\n      isEnabled\n      key\n      title\n      description\n      isAvailable\n      logo\n      website\n      config {\n        key\n        value\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/auth/auth-query-groups.gql",
    "content": "query {\n  groups {\n    list {\n      id\n      name\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/auth/auth-query-host.gql",
    "content": "{\n  site {\n    config {\n      host\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/auth/auth-query-strategies.gql",
    "content": "query {\n  authentication {\n    strategies {\n      isEnabled\n      key\n      title\n      description\n      isAvailable\n      useForm\n      logo\n      website\n      config {\n        key\n        value\n      }\n      selfRegistration\n      domainWhitelist\n      autoEnrollGroups\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/contribute/contribute-query-contributors.gql",
    "content": "query {\n  contribute {\n    contributors {\n      company\n      currency\n      description\n      id\n      image\n      name\n      profile\n      tier\n      totalDonated\n      twitter\n      website\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/dashboard/dashboard-query-stats.gql",
    "content": "query {\n  system {\n    info {\n      currentVersion\n      latestVersion\n      groupsTotal\n      pagesTotal\n      usersTotal\n      tagsTotal\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/dev/dev-mutation-save-flags.gql",
    "content": "mutation (\n  $flags: [SystemFlagInput]!\n) {\n  system {\n    updateFlags(\n      flags: $flags\n    ) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/dev/dev-query-flags.gql",
    "content": "{\n  system {\n    flags {\n      key\n      value\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/groups/groups-mutation-assign.gql",
    "content": "mutation ($groupId: Int!, $userId: Int!) {\n  groups {\n    assignUser(groupId: $groupId, userId: $userId) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/groups/groups-mutation-create.gql",
    "content": "mutation ($name: String!) {\n  groups {\n    create(name: $name) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      group {\n        id\n        name\n        createdAt\n        updatedAt\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/groups/groups-mutation-unassign.gql",
    "content": "mutation ($groupId: Int!, $userId: Int!) {\n  groups {\n    unassignUser(groupId: $groupId, userId: $userId) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/groups/groups-query-list.gql",
    "content": "query {\n  groups {\n    list {\n      id\n      name\n      isSystem\n      userCount\n      createdAt\n      updatedAt\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/locale/locale-mutation-download.gql",
    "content": "mutation($locale: String!) {\n  localization {\n    downloadLocale(locale: $locale) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/locale/locale-mutation-save.gql",
    "content": "mutation($locale: String!, $autoUpdate: Boolean!, $namespacing: Boolean!, $namespaces: [String]!) {\n  localization {\n    updateLocale(locale: $locale, autoUpdate: $autoUpdate, namespacing: $namespacing, namespaces: $namespaces) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/locale/locale-query-list.gql",
    "content": "{\n  localization {\n    locales {\n      availability\n      code\n      createdAt\n      isInstalled\n      installDate\n      isRTL\n      name\n      nativeName\n      updatedAt\n    }\n    config {\n      locale\n      autoUpdate\n      namespacing\n      namespaces\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/logging/logging-mutation-save-loggers.gql",
    "content": "mutation($loggers: [LoggerInput]) {\n  logging {\n    updateLoggers(loggers: $loggers) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/logging/logging-query-loggers.gql",
    "content": "query {\n  logging {\n    loggers(orderBy: \"title ASC\") {\n      isEnabled\n      key\n      title\n      description\n      logo\n      website\n      level\n      config {\n        key\n        value\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/logging/logging-subscription-livetrail.gql",
    "content": "subscription {\n  loggingLiveTrail {\n    level\n    output\n    timestamp\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/mail/mail-mutation-save-config.gql",
    "content": "mutation (\n  $senderName: String!\n  $senderEmail: String!\n  $host: String!\n  $port: Int!\n  $name: String!\n  $secure: Boolean!\n  $verifySSL: Boolean!\n  $user: String!\n  $pass: String!\n  $useDKIM: Boolean!\n  $dkimDomainName: String!\n  $dkimKeySelector: String!\n  $dkimPrivateKey: String!\n) {\n  mail {\n    updateConfig(\n      senderName: $senderName\n      senderEmail: $senderEmail\n      host: $host\n      port: $port\n      name: $name\n      secure: $secure\n      verifySSL: $verifySSL\n      user: $user\n      pass: $pass\n      useDKIM: $useDKIM\n      dkimDomainName: $dkimDomainName\n      dkimKeySelector: $dkimKeySelector\n      dkimPrivateKey: $dkimPrivateKey\n    ) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/mail/mail-mutation-sendtest.gql",
    "content": "mutation ($recipientEmail: String!) {\n  mail {\n    sendTest(recipientEmail: $recipientEmail) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/mail/mail-query-config.gql",
    "content": "{\n  mail {\n    config {\n      senderName\n      senderEmail\n      host\n      port\n      name\n      secure\n      verifySSL\n      user\n      pass\n      useDKIM\n      dkimDomainName\n      dkimKeySelector\n      dkimPrivateKey\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/pages/pages-query-list.gql",
    "content": "query {\n  pages {\n    list {\n      id\n      locale\n      path\n      title\n      description\n      contentType\n      isPublished\n      isPrivate\n      privateNS\n      createdAt\n      updatedAt\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/pages/pages-query-single.gql",
    "content": "query($id: Int!) {\n  pages {\n    single(id:$id) {\n      id\n      path\n      hash\n      title\n      description\n      isPrivate\n      isPublished\n      privateNS\n      publishStartDate\n      publishEndDate\n      contentType\n      createdAt\n      updatedAt\n      editor\n      locale\n      authorId\n      authorName\n      authorEmail\n      creatorId\n      creatorName\n      creatorEmail\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/rendering/rendering-mutation-save-renderers.gql",
    "content": "mutation($renderers: [RendererInput]) {\n  rendering {\n    updateRenderers(renderers: $renderers) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/rendering/rendering-query-renderers.gql",
    "content": "{\n  rendering {\n    renderers {\n      isEnabled\n      key\n      title\n      description\n      icon\n      dependsOn\n      input\n      output\n      config {\n        key\n        value\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/search/search-mutation-rebuild-index.gql",
    "content": "mutation {\n  search {\n    rebuildIndex {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/search/search-mutation-save-engines.gql",
    "content": "mutation($engines: [SearchEngineInput]) {\n  search {\n    updateSearchEngines(engines: $engines) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/search/search-query-engines.gql",
    "content": "query {\n  search {\n    searchEngines(orderBy: \"title\") {\n      isEnabled\n      key\n      title\n      description\n      logo\n      website\n      isAvailable\n      config {\n        key\n        value\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/storage/storage-mutation-executeaction.gql",
    "content": "mutation($targetKey: String!, $handler: String!) {\n  storage {\n    executeAction(targetKey: $targetKey, handler: $handler) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/storage/storage-mutation-save-targets.gql",
    "content": "mutation($targets: [StorageTargetInput]!) {\n  storage {\n    updateTargets(targets: $targets) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/storage/storage-query-status.gql",
    "content": "query {\n  storage {\n    status {\n      key\n      title\n      status\n      message\n      lastAttempt\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/storage/storage-query-targets.gql",
    "content": "query {\n  storage {\n    targets {\n      isAvailable\n      isEnabled\n      key\n      title\n      description\n      logo\n      website\n      supportedModes\n      mode\n      hasSchedule\n      syncInterval\n      syncIntervalDefault\n      config {\n        key\n        value\n      }\n      actions {\n        handler\n        label\n        hint\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/system/system-mutation-upgrade.gql",
    "content": "mutation {\n  system {\n    performUpgrade {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/system/system-query-info.gql",
    "content": "query {\n  system {\n    info {\n      configFile\n      cpuCores\n      currentVersion\n      dbHost\n      dbType\n      dbVersion\n      hostname\n      latestVersion\n      latestVersionReleaseDate\n      nodeVersion\n      operatingSystem\n      platform\n      ramTotal\n      upgradeCapable\n      workingDirectory\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/theme/theme-mutation-save.gql",
    "content": "mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $tocPosition: String, $injectCSS: String, $injectHead: String, $injectBody: String) {\n  theming {\n    setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, tocPosition: $tocPosition, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/theme/theme-query-config.gql",
    "content": "query {\n  theming {\n    config {\n      theme\n      iconset\n      darkMode\n      tocPosition\n      injectCSS\n      injectHead\n      injectBody\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/users/users-mutation-create.gql",
    "content": "mutation ($providerKey: String!, $email: String!, $name: String!, $passwordRaw: String, $groups: [Int]!, $mustChangePassword: Boolean, $sendWelcomeEmail: Boolean) {\n  users {\n    create(providerKey: $providerKey, email: $email, name: $name, passwordRaw: $passwordRaw, groups: $groups, mustChangePassword: $mustChangePassword, sendWelcomeEmail: $sendWelcomeEmail) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/users/users-query-groups.gql",
    "content": "query {\n  groups {\n    list {\n      id\n      name\n      isSystem\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-auth-regencerts.gql",
    "content": "mutation {\n  authentication {\n    regenerateCertificates {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-auth-resetguest.gql",
    "content": "mutation {\n  authentication {\n    resetGuestUser {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-cache-flushcache.gql",
    "content": "mutation {\n  pages {\n    flushCache {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-cache-flushuploads.gql",
    "content": "mutation {\n  assets {\n    flushTempUploads {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-content-migratelocale.gql",
    "content": "mutation($sourceLocale: String!, $targetLocale: String!) {\n  pages {\n    migrateToLocale(sourceLocale: $sourceLocale, targetLocale: $targetLocale) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      count\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-content-rebuildtree.gql",
    "content": "mutation {\n  pages {\n    rebuildTree {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-importv1-users.gql",
    "content": "mutation($mongoDbConnString: String!, $groupMode: SystemImportUsersGroupMode!) {\n  system {\n    importUsersFromV1(mongoDbConnString: $mongoDbConnString, groupMode: $groupMode) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      usersCount\n      groupsCount\n      failed {\n        provider\n        email\n        error\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-telemetry-resetid.gql",
    "content": "mutation {\n  system {\n    resetTelemetryClientId {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-mutation-telemetry-set.gql",
    "content": "mutation($enabled: Boolean!) {\n  system {\n    setTelemetry(enabled: $enabled) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/admin/utilities/utilities-query-telemetry.gql",
    "content": "query {\n  system {\n    info {\n      telemetry\n      telemetryClientId\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-localization-query-translations.gql",
    "content": "query($locale: String!, $namespace: String!) {\n  localization {\n    translations(locale:$locale, namespace:$namespace) {\n      key\n      value\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-pages-mutation-delete.gql",
    "content": "mutation($id: Int!) {\n  pages {\n    delete(id: $id) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-pages-mutation-move.gql",
    "content": "mutation($id: Int!, $destinationPath: String!, $destinationLocale: String!) {\n  pages {\n    move(id: $id, destinationPath: $destinationPath, destinationLocale: $destinationLocale) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-pages-query-list.gql",
    "content": "query ($limit: Int, $orderBy: PageOrderBy, $orderByDirection: PageOrderByDirection, $tags: [String!], $locale: String) {\n  pages {\n    list(limit: $limit, orderBy: $orderBy, orderByDirection: $orderByDirection, tags: $tags, locale: $locale) {\n      id\n      locale\n      path\n      title\n      description\n      createdAt\n      updatedAt\n      tags\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-pages-query-search.gql",
    "content": "query ($query: String!) {\n  pages {\n    search(query:$query) {\n      results {\n        id\n        title\n        description\n        path\n        locale\n      }\n      suggestions\n      totalHits\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-pages-query-tags.gql",
    "content": "query {\n  pages {\n    tags {\n      tag\n      title\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/common/common-pages-query-tree.gql",
    "content": "query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) {\n  pages {\n    tree(parent: $parent, mode: $mode, locale: $locale) {\n      id\n      path\n      title\n      isFolder\n      pageId\n      parent\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/editor/editor-media-mutation-asset-delete.gql",
    "content": "mutation ($id: Int!) {\n  assets {\n    deleteAsset(id: $id) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/editor/editor-media-mutation-asset-rename.gql",
    "content": "mutation ($id: Int!, $filename: String!) {\n  assets {\n    renameAsset(id:$id, filename: $filename) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/editor/editor-media-mutation-folder-create.gql",
    "content": "mutation ($parentFolderId: Int!, $slug: String!) {\n  assets {\n    createFolder(parentFolderId:$parentFolderId, slug: $slug) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/editor/editor-media-query-folder-list.gql",
    "content": "query ($parentFolderId: Int!) {\n  assets {\n    folders(parentFolderId:$parentFolderId) {\n      id\n      name\n      slug\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/editor/editor-media-query-list.gql",
    "content": "query ($folderId: Int!, $kind: AssetKind!) {\n  assets {\n    list(folderId:$folderId, kind: $kind) {\n      id\n      filename\n      ext\n      kind\n      mime\n      fileSize\n      createdAt\n      updatedAt\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/login/login-mutation-changepassword.gql",
    "content": "mutation($continuationToken: String!, $newPassword: String!) {\n  authentication {\n    loginChangePassword(continuationToken: $continuationToken, newPassword: $newPassword) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      jwt\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/login/login-mutation-login.gql",
    "content": "mutation($username: String!, $password: String!, $strategy: String!) {\n  authentication {\n    login(username: $username, password: $password, strategy: $strategy) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      jwt\n      mustChangePwd\n      mustProvideTFA\n      continuationToken\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/login/login-mutation-tfa.gql",
    "content": "mutation($continuationToken: String!, $securityCode: String!) {\n  authentication {\n    loginTFA(continuationToken: $continuationToken, securityCode: $securityCode) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      jwt\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/login/login-query-strategies.gql",
    "content": "query {\n  authentication {\n    strategies(\n      isEnabled: true\n    ) {\n      key\n      title\n      useForm\n      icon\n      color\n      selfRegistration\n    }\n  }\n}\n"
  },
  {
    "path": "client/graph/register/register-mutation-create.gql",
    "content": "mutation($email: String!, $password: String!, $name: String!) {\n  authentication {\n    register(email: $email, password: $password, name: $name) {\n      responseResult {\n        succeeded\n        errorCode\n        slug\n        message\n      }\n      jwt\n    }\n  }\n}\n"
  },
  {
    "path": "client/helpers/compatibility.js",
    "content": "// =======================================\n// Fetch polyfill\n// =======================================\n// Requirement: Safari 9 and below, IE 11 and below\n\nif (!window.fetch) {\n  require('whatwg-fetch')\n}\n"
  },
  {
    "path": "client/helpers/index.js",
    "content": "import filesize from 'filesize.js'\nimport _ from 'lodash'\n\n/* global siteConfig */\n\nconst helpers = {\n  /**\n   * Convert bytes to humanized form\n   * @param {number} rawSize Size in bytes\n   * @returns {string} Humanized file size\n   */\n  filesize (rawSize) {\n    return _.toUpper(filesize(rawSize))\n  },\n  /**\n   * Convert raw path to safe path\n   * @param {string} rawPath Raw path\n   * @returns {string} Safe path\n   */\n  makeSafePath (rawPath) {\n    let rawParts = _.split(_.trim(rawPath), '/')\n    rawParts = _.map(rawParts, (r) => {\n      return _.kebabCase(_.deburr(_.trim(r)))\n    })\n\n    return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/')\n  },\n  resolvePath (path) {\n    if (_.startsWith(path, '/')) { path = path.substring(1) }\n    return `${siteConfig.path}${path}`\n  },\n  /**\n   * Set Input Selection\n   * @param {DOMElement} input The input element\n   * @param {number} startPos The starting position\n   * @param {nunber} endPos The ending position\n   */\n  setInputSelection (input, startPos, endPos) {\n    input.focus()\n    if (typeof input.selectionStart !== 'undefined') {\n      input.selectionStart = startPos\n      input.selectionEnd = endPos\n    } else if (document.selection && document.selection.createRange) {\n      // IE branch\n      input.select()\n      var range = document.selection.createRange()\n      range.collapse(true)\n      range.moveEnd('character', endPos)\n      range.moveStart('character', startPos)\n      range.select()\n    }\n  }\n}\n\nexport default {\n  install(Vue) {\n    Vue.$helpers = helpers\n    Object.defineProperties(Vue.prototype, {\n      $helpers: {\n        get() {\n          return helpers\n        }\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "client/index-app.js",
    "content": "require('core-js/stable')\nrequire('regenerator-runtime/runtime')\n\n/* global siteConfig */\n/* eslint-disable no-unused-expressions */\n\nswitch (window.document.documentElement.lang) {\n  case 'ar':\n  case 'fa':\n    import(/* webpackChunkName: \"fonts-arabic\" */ './scss/fonts/arabic.scss')\n    break\n  default:\n    import(/* webpackChunkName: \"fonts-default\" */ './scss/fonts/default.scss')\n    break\n}\n\nrequire('modernizr')\n\nrequire('./scss/app.scss')\nimport(/* webpackChunkName: \"theme\" */ './themes/' + siteConfig.theme + '/scss/app.scss')\n\nimport(/* webpackChunkName: \"mdi\" */ '@mdi/font/css/materialdesignicons.css')\n\nrequire('./helpers/compatibility.js')\nrequire('./client-app.js')\nimport(/* webpackChunkName: \"theme\" */ './themes/' + siteConfig.theme + '/js/app.js')\n"
  },
  {
    "path": "client/index-legacy.js",
    "content": "require('./scss/legacy.scss')\nrequire('./scss/fonts/default.scss')\n\nwindow.WIKI = null\n"
  },
  {
    "path": "client/index-setup.js",
    "content": "require('core-js/stable')\nrequire('regenerator-runtime/runtime')\n\n/* eslint-disable no-unused-expressions */\n\nrequire('./scss/app.scss')\nimport(/* webpackChunkName: \"mdi\" */ '@mdi/font/css/materialdesignicons.css')\n\nrequire('./helpers/compatibility.js')\n\nrequire('./client-setup.js')\n"
  },
  {
    "path": "client/libs/animate/animate.scss",
    "content": "@charset \"UTF-8\";\n\n/*!\n * animate.css -http://daneden.me/animate\n * Version - 3.5.1\n * Licensed under the MIT license - http://opensource.org/licenses/MIT\n *\n * Copyright (c) 2016 Daniel Eden\n */\n\n.animated {\n  -webkit-animation-duration: 1s;\n  animation-duration: 1s;\n  -webkit-animation-fill-mode: both;\n  animation-fill-mode: both;\n  &.infinite {\n    -webkit-animation-iteration-count: infinite;\n    animation-iteration-count: infinite;\n  }\n  &.hinge {\n    -webkit-animation-duration: 2s;\n    animation-duration: 2s;\n  }\n  &.flipOutX, &.flipOutY, &.bounceIn, &.bounceOut {\n    -webkit-animation-duration: .75s;\n    animation-duration: .75s;\n  }\n}\n\n@-webkit-keyframes bounce {\n  from, 20%, 53%, 80%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  40%, 43% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -30px, 0);\n    transform: translate3d(0, -30px, 0);\n  }\n\n  70% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -15px, 0);\n    transform: translate3d(0, -15px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, -4px, 0);\n    transform: translate3d(0, -4px, 0);\n  }\n}\n\n\n@keyframes bounce {\n  from, 20%, 53%, 80%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  40%, 43% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -30px, 0);\n    transform: translate3d(0, -30px, 0);\n  }\n\n  70% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -15px, 0);\n    transform: translate3d(0, -15px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, -4px, 0);\n    transform: translate3d(0, -4px, 0);\n  }\n}\n\n\n.bounce {\n  -webkit-animation-name: bounce;\n  animation-name: bounce;\n  -webkit-transform-origin: center bottom;\n  transform-origin: center bottom;\n}\n\n@-webkit-keyframes flash {\n  from, 50%, to {\n    opacity: 1;\n  }\n\n  25%, 75% {\n    opacity: 0;\n  }\n}\n\n\n@keyframes flash {\n  from, 50%, to {\n    opacity: 1;\n  }\n\n  25%, 75% {\n    opacity: 0;\n  }\n}\n\n\n.flash {\n  -webkit-animation-name: flash;\n  animation-name: flash;\n}\n\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes pulse {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.05, 1.05, 1.05);\n    transform: scale3d(1.05, 1.05, 1.05);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n@keyframes pulse {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.05, 1.05, 1.05);\n    transform: scale3d(1.05, 1.05, 1.05);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n.pulse {\n  -webkit-animation-name: pulse;\n  animation-name: pulse;\n}\n\n@-webkit-keyframes rubberBand {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  30% {\n    -webkit-transform: scale3d(1.25, 0.75, 1);\n    transform: scale3d(1.25, 0.75, 1);\n  }\n\n  40% {\n    -webkit-transform: scale3d(0.75, 1.25, 1);\n    transform: scale3d(0.75, 1.25, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.15, 0.85, 1);\n    transform: scale3d(1.15, 0.85, 1);\n  }\n\n  65% {\n    -webkit-transform: scale3d(0.95, 1.05, 1);\n    transform: scale3d(0.95, 1.05, 1);\n  }\n\n  75% {\n    -webkit-transform: scale3d(1.05, 0.95, 1);\n    transform: scale3d(1.05, 0.95, 1);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n@keyframes rubberBand {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  30% {\n    -webkit-transform: scale3d(1.25, 0.75, 1);\n    transform: scale3d(1.25, 0.75, 1);\n  }\n\n  40% {\n    -webkit-transform: scale3d(0.75, 1.25, 1);\n    transform: scale3d(0.75, 1.25, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.15, 0.85, 1);\n    transform: scale3d(1.15, 0.85, 1);\n  }\n\n  65% {\n    -webkit-transform: scale3d(0.95, 1.05, 1);\n    transform: scale3d(0.95, 1.05, 1);\n  }\n\n  75% {\n    -webkit-transform: scale3d(1.05, 0.95, 1);\n    transform: scale3d(1.05, 0.95, 1);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n.rubberBand {\n  -webkit-animation-name: rubberBand;\n  animation-name: rubberBand;\n}\n\n@-webkit-keyframes shake {\n  from, to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  10%, 30%, 50%, 70%, 90% {\n    -webkit-transform: translate3d(-10px, 0, 0);\n    transform: translate3d(-10px, 0, 0);\n  }\n\n  20%, 40%, 60%, 80% {\n    -webkit-transform: translate3d(10px, 0, 0);\n    transform: translate3d(10px, 0, 0);\n  }\n}\n\n\n@keyframes shake {\n  from, to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  10%, 30%, 50%, 70%, 90% {\n    -webkit-transform: translate3d(-10px, 0, 0);\n    transform: translate3d(-10px, 0, 0);\n  }\n\n  20%, 40%, 60%, 80% {\n    -webkit-transform: translate3d(10px, 0, 0);\n    transform: translate3d(10px, 0, 0);\n  }\n}\n\n\n.shake {\n  -webkit-animation-name: shake;\n  animation-name: shake;\n}\n\n@-webkit-keyframes headShake {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n\n  6.5% {\n    -webkit-transform: translateX(-6px) rotateY(-9deg);\n    transform: translateX(-6px) rotateY(-9deg);\n  }\n\n  18.5% {\n    -webkit-transform: translateX(5px) rotateY(7deg);\n    transform: translateX(5px) rotateY(7deg);\n  }\n\n  31.5% {\n    -webkit-transform: translateX(-3px) rotateY(-5deg);\n    transform: translateX(-3px) rotateY(-5deg);\n  }\n\n  43.5% {\n    -webkit-transform: translateX(2px) rotateY(3deg);\n    transform: translateX(2px) rotateY(3deg);\n  }\n\n  50% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n\n\n@keyframes headShake {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n\n  6.5% {\n    -webkit-transform: translateX(-6px) rotateY(-9deg);\n    transform: translateX(-6px) rotateY(-9deg);\n  }\n\n  18.5% {\n    -webkit-transform: translateX(5px) rotateY(7deg);\n    transform: translateX(5px) rotateY(7deg);\n  }\n\n  31.5% {\n    -webkit-transform: translateX(-3px) rotateY(-5deg);\n    transform: translateX(-3px) rotateY(-5deg);\n  }\n\n  43.5% {\n    -webkit-transform: translateX(2px) rotateY(3deg);\n    transform: translateX(2px) rotateY(3deg);\n  }\n\n  50% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n\n\n.headShake {\n  -webkit-animation-timing-function: ease-in-out;\n  animation-timing-function: ease-in-out;\n  -webkit-animation-name: headShake;\n  animation-name: headShake;\n}\n\n@-webkit-keyframes swing {\n  20% {\n    -webkit-transform: rotate3d(0, 0, 1, 15deg);\n    transform: rotate3d(0, 0, 1, 15deg);\n  }\n\n  40% {\n    -webkit-transform: rotate3d(0, 0, 1, -10deg);\n    transform: rotate3d(0, 0, 1, -10deg);\n  }\n\n  60% {\n    -webkit-transform: rotate3d(0, 0, 1, 5deg);\n    transform: rotate3d(0, 0, 1, 5deg);\n  }\n\n  80% {\n    -webkit-transform: rotate3d(0, 0, 1, -5deg);\n    transform: rotate3d(0, 0, 1, -5deg);\n  }\n\n  to {\n    -webkit-transform: rotate3d(0, 0, 1, 0deg);\n    transform: rotate3d(0, 0, 1, 0deg);\n  }\n}\n\n\n@keyframes swing {\n  20% {\n    -webkit-transform: rotate3d(0, 0, 1, 15deg);\n    transform: rotate3d(0, 0, 1, 15deg);\n  }\n\n  40% {\n    -webkit-transform: rotate3d(0, 0, 1, -10deg);\n    transform: rotate3d(0, 0, 1, -10deg);\n  }\n\n  60% {\n    -webkit-transform: rotate3d(0, 0, 1, 5deg);\n    transform: rotate3d(0, 0, 1, 5deg);\n  }\n\n  80% {\n    -webkit-transform: rotate3d(0, 0, 1, -5deg);\n    transform: rotate3d(0, 0, 1, -5deg);\n  }\n\n  to {\n    -webkit-transform: rotate3d(0, 0, 1, 0deg);\n    transform: rotate3d(0, 0, 1, 0deg);\n  }\n}\n\n\n.swing {\n  -webkit-transform-origin: top center;\n  transform-origin: top center;\n  -webkit-animation-name: swing;\n  animation-name: swing;\n}\n\n@-webkit-keyframes tada {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  10%, 20% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n  }\n\n  30%, 50%, 70%, 90% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n  }\n\n  40%, 60%, 80% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n@keyframes tada {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  10%, 20% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n  }\n\n  30%, 50%, 70%, 90% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n  }\n\n  40%, 60%, 80% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n.tada {\n  -webkit-animation-name: tada;\n  animation-name: tada;\n}\n\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes wobble {\n  from {\n    -webkit-transform: none;\n    transform: none;\n  }\n\n  15% {\n    -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n    transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n  }\n\n  30% {\n    -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n    transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n  }\n\n  45% {\n    -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n    transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n  }\n\n  60% {\n    -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n    transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n  }\n\n  75% {\n    -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n    transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes wobble {\n  from {\n    -webkit-transform: none;\n    transform: none;\n  }\n\n  15% {\n    -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n    transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n  }\n\n  30% {\n    -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n    transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n  }\n\n  45% {\n    -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n    transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n  }\n\n  60% {\n    -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n    transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n  }\n\n  75% {\n    -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n    transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.wobble {\n  -webkit-animation-name: wobble;\n  animation-name: wobble;\n}\n\n@-webkit-keyframes jello {\n  from, 11.1%, to {\n    -webkit-transform: none;\n    transform: none;\n  }\n\n  22.2% {\n    -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);\n    transform: skewX(-12.5deg) skewY(-12.5deg);\n  }\n\n  33.3% {\n    -webkit-transform: skewX(6.25deg) skewY(6.25deg);\n    transform: skewX(6.25deg) skewY(6.25deg);\n  }\n\n  44.4% {\n    -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);\n    transform: skewX(-3.125deg) skewY(-3.125deg);\n  }\n\n  55.5% {\n    -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);\n    transform: skewX(1.5625deg) skewY(1.5625deg);\n  }\n\n  66.6% {\n    -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);\n    transform: skewX(-0.78125deg) skewY(-0.78125deg);\n  }\n\n  77.7% {\n    -webkit-transform: skewX(0.39063deg) skewY(0.39063deg);\n    transform: skewX(0.39063deg) skewY(0.39063deg);\n  }\n\n  88.8% {\n    -webkit-transform: skewX(-0.19531deg) skewY(-0.19531deg);\n    transform: skewX(-0.19531deg) skewY(-0.19531deg);\n  }\n}\n\n\n@keyframes jello {\n  from, 11.1%, to {\n    -webkit-transform: none;\n    transform: none;\n  }\n\n  22.2% {\n    -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);\n    transform: skewX(-12.5deg) skewY(-12.5deg);\n  }\n\n  33.3% {\n    -webkit-transform: skewX(6.25deg) skewY(6.25deg);\n    transform: skewX(6.25deg) skewY(6.25deg);\n  }\n\n  44.4% {\n    -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);\n    transform: skewX(-3.125deg) skewY(-3.125deg);\n  }\n\n  55.5% {\n    -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);\n    transform: skewX(1.5625deg) skewY(1.5625deg);\n  }\n\n  66.6% {\n    -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);\n    transform: skewX(-0.78125deg) skewY(-0.78125deg);\n  }\n\n  77.7% {\n    -webkit-transform: skewX(0.39063deg) skewY(0.39063deg);\n    transform: skewX(0.39063deg) skewY(0.39063deg);\n  }\n\n  88.8% {\n    -webkit-transform: skewX(-0.19531deg) skewY(-0.19531deg);\n    transform: skewX(-0.19531deg) skewY(-0.19531deg);\n  }\n}\n\n\n.jello {\n  -webkit-animation-name: jello;\n  animation-name: jello;\n  -webkit-transform-origin: center;\n  transform-origin: center;\n}\n\n@-webkit-keyframes bounceIn {\n  from, 20%, 40%, 60%, 80%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  20% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1);\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  40% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9);\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(1.03, 1.03, 1.03);\n    transform: scale3d(1.03, 1.03, 1.03);\n  }\n\n  80% {\n    -webkit-transform: scale3d(0.97, 0.97, 0.97);\n    transform: scale3d(0.97, 0.97, 0.97);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n@keyframes bounceIn {\n  from, 20%, 40%, 60%, 80%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  20% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1);\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  40% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9);\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(1.03, 1.03, 1.03);\n    transform: scale3d(1.03, 1.03, 1.03);\n  }\n\n  80% {\n    -webkit-transform: scale3d(0.97, 0.97, 0.97);\n    transform: scale3d(0.97, 0.97, 0.97);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n\n.bounceIn {\n  -webkit-animation-name: bounceIn;\n  animation-name: bounceIn;\n}\n\n@-webkit-keyframes bounceInDown {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -3000px, 0);\n    transform: translate3d(0, -3000px, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, 25px, 0);\n    transform: translate3d(0, 25px, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(0, -10px, 0);\n    transform: translate3d(0, -10px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, 5px, 0);\n    transform: translate3d(0, 5px, 0);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes bounceInDown {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -3000px, 0);\n    transform: translate3d(0, -3000px, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, 25px, 0);\n    transform: translate3d(0, 25px, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(0, -10px, 0);\n    transform: translate3d(0, -10px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, 5px, 0);\n    transform: translate3d(0, 5px, 0);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.bounceInDown {\n  -webkit-animation-name: bounceInDown;\n  animation-name: bounceInDown;\n}\n\n@-webkit-keyframes bounceInLeft {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    -webkit-transform: translate3d(-3000px, 0, 0);\n    transform: translate3d(-3000px, 0, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(25px, 0, 0);\n    transform: translate3d(25px, 0, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(-10px, 0, 0);\n    transform: translate3d(-10px, 0, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(5px, 0, 0);\n    transform: translate3d(5px, 0, 0);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes bounceInLeft {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    -webkit-transform: translate3d(-3000px, 0, 0);\n    transform: translate3d(-3000px, 0, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(25px, 0, 0);\n    transform: translate3d(25px, 0, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(-10px, 0, 0);\n    transform: translate3d(-10px, 0, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(5px, 0, 0);\n    transform: translate3d(5px, 0, 0);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.bounceInLeft {\n  -webkit-animation-name: bounceInLeft;\n  animation-name: bounceInLeft;\n}\n\n@-webkit-keyframes bounceInRight {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(3000px, 0, 0);\n    transform: translate3d(3000px, 0, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(-25px, 0, 0);\n    transform: translate3d(-25px, 0, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(10px, 0, 0);\n    transform: translate3d(10px, 0, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(-5px, 0, 0);\n    transform: translate3d(-5px, 0, 0);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes bounceInRight {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(3000px, 0, 0);\n    transform: translate3d(3000px, 0, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(-25px, 0, 0);\n    transform: translate3d(-25px, 0, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(10px, 0, 0);\n    transform: translate3d(10px, 0, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(-5px, 0, 0);\n    transform: translate3d(-5px, 0, 0);\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.bounceInRight {\n  -webkit-animation-name: bounceInRight;\n  animation-name: bounceInRight;\n}\n\n@-webkit-keyframes bounceInUp {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 3000px, 0);\n    transform: translate3d(0, 3000px, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, -20px, 0);\n    transform: translate3d(0, -20px, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(0, 10px, 0);\n    transform: translate3d(0, 10px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, -5px, 0);\n    transform: translate3d(0, -5px, 0);\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n@keyframes bounceInUp {\n  from, 60%, 75%, 90%, to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 3000px, 0);\n    transform: translate3d(0, 3000px, 0);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, -20px, 0);\n    transform: translate3d(0, -20px, 0);\n  }\n\n  75% {\n    -webkit-transform: translate3d(0, 10px, 0);\n    transform: translate3d(0, 10px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, -5px, 0);\n    transform: translate3d(0, -5px, 0);\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n.bounceInUp {\n  -webkit-animation-name: bounceInUp;\n  animation-name: bounceInUp;\n}\n\n@-webkit-keyframes bounceOut {\n  20% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9);\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  50%, 55% {\n    opacity: 1;\n    -webkit-transform: scale3d(1.1, 1.1, 1.1);\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n}\n\n\n@keyframes bounceOut {\n  20% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9);\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  50%, 55% {\n    opacity: 1;\n    -webkit-transform: scale3d(1.1, 1.1, 1.1);\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n}\n\n\n.bounceOut {\n  -webkit-animation-name: bounceOut;\n  animation-name: bounceOut;\n}\n\n@-webkit-keyframes bounceOutDown {\n  20% {\n    -webkit-transform: translate3d(0, 10px, 0);\n    transform: translate3d(0, 10px, 0);\n  }\n\n  40%, 45% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, -20px, 0);\n    transform: translate3d(0, -20px, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 2000px, 0);\n    transform: translate3d(0, 2000px, 0);\n  }\n}\n\n\n@keyframes bounceOutDown {\n  20% {\n    -webkit-transform: translate3d(0, 10px, 0);\n    transform: translate3d(0, 10px, 0);\n  }\n\n  40%, 45% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, -20px, 0);\n    transform: translate3d(0, -20px, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 2000px, 0);\n    transform: translate3d(0, 2000px, 0);\n  }\n}\n\n\n.bounceOutDown {\n  -webkit-animation-name: bounceOutDown;\n  animation-name: bounceOutDown;\n}\n\n@-webkit-keyframes bounceOutLeft {\n  20% {\n    opacity: 1;\n    -webkit-transform: translate3d(20px, 0, 0);\n    transform: translate3d(20px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(-2000px, 0, 0);\n    transform: translate3d(-2000px, 0, 0);\n  }\n}\n\n\n@keyframes bounceOutLeft {\n  20% {\n    opacity: 1;\n    -webkit-transform: translate3d(20px, 0, 0);\n    transform: translate3d(20px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(-2000px, 0, 0);\n    transform: translate3d(-2000px, 0, 0);\n  }\n}\n\n\n.bounceOutLeft {\n  -webkit-animation-name: bounceOutLeft;\n  animation-name: bounceOutLeft;\n}\n\n@-webkit-keyframes bounceOutRight {\n  20% {\n    opacity: 1;\n    -webkit-transform: translate3d(-20px, 0, 0);\n    transform: translate3d(-20px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(2000px, 0, 0);\n    transform: translate3d(2000px, 0, 0);\n  }\n}\n\n\n@keyframes bounceOutRight {\n  20% {\n    opacity: 1;\n    -webkit-transform: translate3d(-20px, 0, 0);\n    transform: translate3d(-20px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(2000px, 0, 0);\n    transform: translate3d(2000px, 0, 0);\n  }\n}\n\n\n.bounceOutRight {\n  -webkit-animation-name: bounceOutRight;\n  animation-name: bounceOutRight;\n}\n\n@-webkit-keyframes bounceOutUp {\n  20% {\n    -webkit-transform: translate3d(0, -10px, 0);\n    transform: translate3d(0, -10px, 0);\n  }\n\n  40%, 45% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, 20px, 0);\n    transform: translate3d(0, 20px, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -2000px, 0);\n    transform: translate3d(0, -2000px, 0);\n  }\n}\n\n\n@keyframes bounceOutUp {\n  20% {\n    -webkit-transform: translate3d(0, -10px, 0);\n    transform: translate3d(0, -10px, 0);\n  }\n\n  40%, 45% {\n    opacity: 1;\n    -webkit-transform: translate3d(0, 20px, 0);\n    transform: translate3d(0, 20px, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -2000px, 0);\n    transform: translate3d(0, -2000px, 0);\n  }\n}\n\n\n.bounceOutUp {\n  -webkit-animation-name: bounceOutUp;\n  animation-name: bounceOutUp;\n}\n\n@-webkit-keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n\n.fadeIn {\n  -webkit-animation-name: fadeIn;\n  animation-name: fadeIn;\n}\n\n@-webkit-keyframes fadeInDown {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInDown {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInDown {\n  -webkit-animation-name: fadeInDown;\n  animation-name: fadeInDown;\n}\n\n@-webkit-keyframes fadeInDownBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -2000px, 0);\n    transform: translate3d(0, -2000px, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInDownBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -2000px, 0);\n    transform: translate3d(0, -2000px, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInDownBig {\n  -webkit-animation-name: fadeInDownBig;\n  animation-name: fadeInDownBig;\n}\n\n@-webkit-keyframes fadeInLeft {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInLeft {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInLeft {\n  -webkit-animation-name: fadeInLeft;\n  animation-name: fadeInLeft;\n}\n\n@-webkit-keyframes fadeInLeftBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(-2000px, 0, 0);\n    transform: translate3d(-2000px, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInLeftBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(-2000px, 0, 0);\n    transform: translate3d(-2000px, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInLeftBig {\n  -webkit-animation-name: fadeInLeftBig;\n  animation-name: fadeInLeftBig;\n}\n\n@-webkit-keyframes fadeInRight {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInRight {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInRight {\n  -webkit-animation-name: fadeInRight;\n  animation-name: fadeInRight;\n}\n\n@-webkit-keyframes fadeInRightBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(2000px, 0, 0);\n    transform: translate3d(2000px, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInRightBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(2000px, 0, 0);\n    transform: translate3d(2000px, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInRightBig {\n  -webkit-animation-name: fadeInRightBig;\n  animation-name: fadeInRightBig;\n}\n\n@-webkit-keyframes fadeInUp {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInUp {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInUp {\n  -webkit-animation-name: fadeInUp;\n  animation-name: fadeInUp;\n}\n\n@-webkit-keyframes fadeInUpBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 2000px, 0);\n    transform: translate3d(0, 2000px, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes fadeInUpBig {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 2000px, 0);\n    transform: translate3d(0, 2000px, 0);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.fadeInUpBig {\n  -webkit-animation-name: fadeInUpBig;\n  animation-name: fadeInUpBig;\n}\n\n@-webkit-keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n\n\n@keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n\n\n.fadeOut {\n  -webkit-animation-name: fadeOut;\n  animation-name: fadeOut;\n}\n\n@-webkit-keyframes fadeOutDown {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n  }\n}\n\n\n@keyframes fadeOutDown {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n  }\n}\n\n\n.fadeOutDown {\n  -webkit-animation-name: fadeOutDown;\n  animation-name: fadeOutDown;\n}\n\n@-webkit-keyframes fadeOutDownBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 2000px, 0);\n    transform: translate3d(0, 2000px, 0);\n  }\n}\n\n\n@keyframes fadeOutDownBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, 2000px, 0);\n    transform: translate3d(0, 2000px, 0);\n  }\n}\n\n\n.fadeOutDownBig {\n  -webkit-animation-name: fadeOutDownBig;\n  animation-name: fadeOutDownBig;\n}\n\n@-webkit-keyframes fadeOutLeft {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n  }\n}\n\n\n@keyframes fadeOutLeft {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n  }\n}\n\n\n.fadeOutLeft {\n  -webkit-animation-name: fadeOutLeft;\n  animation-name: fadeOutLeft;\n}\n\n@-webkit-keyframes fadeOutLeftBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(-2000px, 0, 0);\n    transform: translate3d(-2000px, 0, 0);\n  }\n}\n\n\n@keyframes fadeOutLeftBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(-2000px, 0, 0);\n    transform: translate3d(-2000px, 0, 0);\n  }\n}\n\n\n.fadeOutLeftBig {\n  -webkit-animation-name: fadeOutLeftBig;\n  animation-name: fadeOutLeftBig;\n}\n\n@-webkit-keyframes fadeOutRight {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n  }\n}\n\n\n@keyframes fadeOutRight {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n  }\n}\n\n\n.fadeOutRight {\n  -webkit-animation-name: fadeOutRight;\n  animation-name: fadeOutRight;\n}\n\n@-webkit-keyframes fadeOutRightBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(2000px, 0, 0);\n    transform: translate3d(2000px, 0, 0);\n  }\n}\n\n\n@keyframes fadeOutRightBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(2000px, 0, 0);\n    transform: translate3d(2000px, 0, 0);\n  }\n}\n\n\n.fadeOutRightBig {\n  -webkit-animation-name: fadeOutRightBig;\n  animation-name: fadeOutRightBig;\n}\n\n@-webkit-keyframes fadeOutUp {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n  }\n}\n\n\n@keyframes fadeOutUp {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n  }\n}\n\n\n.fadeOutUp {\n  -webkit-animation-name: fadeOutUp;\n  animation-name: fadeOutUp;\n}\n\n@-webkit-keyframes fadeOutUpBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -2000px, 0);\n    transform: translate3d(0, -2000px, 0);\n  }\n}\n\n\n@keyframes fadeOutUpBig {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -2000px, 0);\n    transform: translate3d(0, -2000px, 0);\n  }\n}\n\n\n.fadeOutUpBig {\n  -webkit-animation-name: fadeOutUpBig;\n  animation-name: fadeOutUpBig;\n}\n\n@-webkit-keyframes flip {\n  from {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -360deg);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n\n  40% {\n    -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);\n    transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n\n  50% {\n    -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);\n    transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  80% {\n    -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95);\n    transform: perspective(400px) scale3d(0.95, 0.95, 0.95);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  to {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n}\n\n\n@keyframes flip {\n  from {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -360deg);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n\n  40% {\n    -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);\n    transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n\n  50% {\n    -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);\n    transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  80% {\n    -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95);\n    transform: perspective(400px) scale3d(0.95, 0.95, 0.95);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  to {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n}\n\n\n.animated.flip {\n  -webkit-backface-visibility: visible;\n  backface-visibility: visible;\n  -webkit-animation-name: flip;\n  animation-name: flip;\n}\n\n@-webkit-keyframes flipInX {\n  from {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n  }\n\n  to {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n}\n\n\n@keyframes flipInX {\n  from {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n  }\n\n  to {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n}\n\n\n.flipInX {\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  -webkit-animation-name: flipInX;\n  animation-name: flipInX;\n}\n\n@-webkit-keyframes flipInY {\n  from {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -20deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -5deg);\n  }\n\n  to {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n}\n\n\n@keyframes flipInY {\n  from {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -20deg);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -5deg);\n  }\n\n  to {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n}\n\n\n.flipInY {\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  -webkit-animation-name: flipInY;\n  animation-name: flipInY;\n}\n\n@-webkit-keyframes flipOutX {\n  from {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n\n  30% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes flipOutX {\n  from {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n\n  30% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    opacity: 0;\n  }\n}\n\n\n.flipOutX {\n  -webkit-animation-name: flipOutX;\n  animation-name: flipOutX;\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n}\n\n@-webkit-keyframes flipOutY {\n  from {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n\n  30% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -15deg);\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes flipOutY {\n  from {\n    -webkit-transform: perspective(400px);\n    transform: perspective(400px);\n  }\n\n  30% {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, -15deg);\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    opacity: 0;\n  }\n}\n\n\n.flipOutY {\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  -webkit-animation-name: flipOutY;\n  animation-name: flipOutY;\n}\n\n@-webkit-keyframes lightSpeedIn {\n  from {\n    -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);\n    transform: translate3d(100%, 0, 0) skewX(-30deg);\n    opacity: 0;\n  }\n\n  60% {\n    -webkit-transform: skewX(20deg);\n    transform: skewX(20deg);\n    opacity: 1;\n  }\n\n  80% {\n    -webkit-transform: skewX(-5deg);\n    transform: skewX(-5deg);\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n@keyframes lightSpeedIn {\n  from {\n    -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);\n    transform: translate3d(100%, 0, 0) skewX(-30deg);\n    opacity: 0;\n  }\n\n  60% {\n    -webkit-transform: skewX(20deg);\n    transform: skewX(20deg);\n    opacity: 1;\n  }\n\n  80% {\n    -webkit-transform: skewX(-5deg);\n    transform: skewX(-5deg);\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n.lightSpeedIn {\n  -webkit-animation-name: lightSpeedIn;\n  animation-name: lightSpeedIn;\n  -webkit-animation-timing-function: ease-out;\n  animation-timing-function: ease-out;\n}\n\n@-webkit-keyframes lightSpeedOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);\n    transform: translate3d(100%, 0, 0) skewX(30deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes lightSpeedOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);\n    transform: translate3d(100%, 0, 0) skewX(30deg);\n    opacity: 0;\n  }\n}\n\n\n.lightSpeedOut {\n  -webkit-animation-name: lightSpeedOut;\n  animation-name: lightSpeedOut;\n  -webkit-animation-timing-function: ease-in;\n  animation-timing-function: ease-in;\n}\n\n@-webkit-keyframes rotateIn {\n  from {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: rotate3d(0, 0, 1, -200deg);\n    transform: rotate3d(0, 0, 1, -200deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n@keyframes rotateIn {\n  from {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: rotate3d(0, 0, 1, -200deg);\n    transform: rotate3d(0, 0, 1, -200deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n.rotateIn {\n  -webkit-animation-name: rotateIn;\n  animation-name: rotateIn;\n}\n\n@-webkit-keyframes rotateInDownLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -45deg);\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n@keyframes rotateInDownLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -45deg);\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n.rotateInDownLeft {\n  -webkit-animation-name: rotateInDownLeft;\n  animation-name: rotateInDownLeft;\n}\n\n@-webkit-keyframes rotateInDownRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 45deg);\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n@keyframes rotateInDownRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 45deg);\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n.rotateInDownRight {\n  -webkit-animation-name: rotateInDownRight;\n  animation-name: rotateInDownRight;\n}\n\n@-webkit-keyframes rotateInUpLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 45deg);\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n@keyframes rotateInUpLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 45deg);\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n.rotateInUpLeft {\n  -webkit-animation-name: rotateInUpLeft;\n  animation-name: rotateInUpLeft;\n}\n\n@-webkit-keyframes rotateInUpRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -90deg);\n    transform: rotate3d(0, 0, 1, -90deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n@keyframes rotateInUpRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -90deg);\n    transform: rotate3d(0, 0, 1, -90deg);\n    opacity: 0;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: none;\n    transform: none;\n    opacity: 1;\n  }\n}\n\n\n.rotateInUpRight {\n  -webkit-animation-name: rotateInUpRight;\n  animation-name: rotateInUpRight;\n}\n\n@-webkit-keyframes rotateOut {\n  from {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: rotate3d(0, 0, 1, 200deg);\n    transform: rotate3d(0, 0, 1, 200deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes rotateOut {\n  from {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: rotate3d(0, 0, 1, 200deg);\n    transform: rotate3d(0, 0, 1, 200deg);\n    opacity: 0;\n  }\n}\n\n\n.rotateOut {\n  -webkit-animation-name: rotateOut;\n  animation-name: rotateOut;\n}\n\n@-webkit-keyframes rotateOutDownLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 45deg);\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes rotateOutDownLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 45deg);\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n}\n\n\n.rotateOutDownLeft {\n  -webkit-animation-name: rotateOutDownLeft;\n  animation-name: rotateOutDownLeft;\n}\n\n@-webkit-keyframes rotateOutDownRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -45deg);\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes rotateOutDownRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -45deg);\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n\n\n.rotateOutDownRight {\n  -webkit-animation-name: rotateOutDownRight;\n  animation-name: rotateOutDownRight;\n}\n\n@-webkit-keyframes rotateOutUpLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -45deg);\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes rotateOutUpLeft {\n  from {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate3d(0, 0, 1, -45deg);\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n\n\n.rotateOutUpLeft {\n  -webkit-animation-name: rotateOutUpLeft;\n  animation-name: rotateOutUpLeft;\n}\n\n@-webkit-keyframes rotateOutUpRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 90deg);\n    transform: rotate3d(0, 0, 1, 90deg);\n    opacity: 0;\n  }\n}\n\n\n@keyframes rotateOutUpRight {\n  from {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate3d(0, 0, 1, 90deg);\n    transform: rotate3d(0, 0, 1, 90deg);\n    opacity: 0;\n  }\n}\n\n\n.rotateOutUpRight {\n  -webkit-animation-name: rotateOutUpRight;\n  animation-name: rotateOutUpRight;\n}\n\n@-webkit-keyframes hinge {\n  0% {\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n\n  20%, 60% {\n    -webkit-transform: rotate3d(0, 0, 1, 80deg);\n    transform: rotate3d(0, 0, 1, 80deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n\n  40%, 80% {\n    -webkit-transform: rotate3d(0, 0, 1, 60deg);\n    transform: rotate3d(0, 0, 1, 60deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 700px, 0);\n    transform: translate3d(0, 700px, 0);\n    opacity: 0;\n  }\n}\n\n\n@keyframes hinge {\n  0% {\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n\n  20%, 60% {\n    -webkit-transform: rotate3d(0, 0, 1, 80deg);\n    transform: rotate3d(0, 0, 1, 80deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n\n  40%, 80% {\n    -webkit-transform: rotate3d(0, 0, 1, 60deg);\n    transform: rotate3d(0, 0, 1, 60deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 700px, 0);\n    transform: translate3d(0, 700px, 0);\n    opacity: 0;\n  }\n}\n\n\n.hinge {\n  -webkit-animation-name: hinge;\n  animation-name: hinge;\n}\n\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes rollIn {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);\n    transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n@keyframes rollIn {\n  from {\n    opacity: 0;\n    -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);\n    transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);\n  }\n\n  to {\n    opacity: 1;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.rollIn {\n  -webkit-animation-name: rollIn;\n  animation-name: rollIn;\n}\n\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes rollOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);\n    transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);\n  }\n}\n\n\n@keyframes rollOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);\n    transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);\n  }\n}\n\n\n.rollOut {\n  -webkit-animation-name: rollOut;\n  animation-name: rollOut;\n}\n\n@-webkit-keyframes zoomIn {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  50% {\n    opacity: 1;\n  }\n}\n\n\n@keyframes zoomIn {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  50% {\n    opacity: 1;\n  }\n}\n\n\n.zoomIn {\n  -webkit-animation-name: zoomIn;\n  animation-name: zoomIn;\n}\n\n@-webkit-keyframes zoomInDown {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n@keyframes zoomInDown {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n.zoomInDown {\n  -webkit-animation-name: zoomInDown;\n  animation-name: zoomInDown;\n}\n\n@-webkit-keyframes zoomInLeft {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n@keyframes zoomInLeft {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n.zoomInLeft {\n  -webkit-animation-name: zoomInLeft;\n  animation-name: zoomInLeft;\n}\n\n@-webkit-keyframes zoomInRight {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n@keyframes zoomInRight {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n.zoomInRight {\n  -webkit-animation-name: zoomInRight;\n  animation-name: zoomInRight;\n}\n\n@-webkit-keyframes zoomInUp {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n@keyframes zoomInUp {\n  from {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n.zoomInUp {\n  -webkit-animation-name: zoomInUp;\n  animation-name: zoomInUp;\n}\n\n@-webkit-keyframes zoomOut {\n  from {\n    opacity: 1;\n  }\n\n  50% {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n\n\n@keyframes zoomOut {\n  from {\n    opacity: 1;\n  }\n\n  50% {\n    opacity: 0;\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n\n\n.zoomOut {\n  -webkit-animation-name: zoomOut;\n  animation-name: zoomOut;\n}\n\n@-webkit-keyframes zoomOutDown {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);\n    -webkit-transform-origin: center bottom;\n    transform-origin: center bottom;\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n@keyframes zoomOutDown {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);\n    -webkit-transform-origin: center bottom;\n    transform-origin: center bottom;\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n.zoomOutDown {\n  -webkit-animation-name: zoomOutDown;\n  animation-name: zoomOutDown;\n}\n\n@-webkit-keyframes zoomOutLeft {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0);\n    transform: scale(0.1) translate3d(-2000px, 0, 0);\n    -webkit-transform-origin: left center;\n    transform-origin: left center;\n  }\n}\n\n\n@keyframes zoomOutLeft {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0);\n    transform: scale(0.1) translate3d(-2000px, 0, 0);\n    -webkit-transform-origin: left center;\n    transform-origin: left center;\n  }\n}\n\n\n.zoomOutLeft {\n  -webkit-animation-name: zoomOutLeft;\n  animation-name: zoomOutLeft;\n}\n\n@-webkit-keyframes zoomOutRight {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale(0.1) translate3d(2000px, 0, 0);\n    transform: scale(0.1) translate3d(2000px, 0, 0);\n    -webkit-transform-origin: right center;\n    transform-origin: right center;\n  }\n}\n\n\n@keyframes zoomOutRight {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale(0.1) translate3d(2000px, 0, 0);\n    transform: scale(0.1) translate3d(2000px, 0, 0);\n    -webkit-transform-origin: right center;\n    transform-origin: right center;\n  }\n}\n\n\n.zoomOutRight {\n  -webkit-animation-name: zoomOutRight;\n  animation-name: zoomOutRight;\n}\n\n@-webkit-keyframes zoomOutUp {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);\n    -webkit-transform-origin: center bottom;\n    transform-origin: center bottom;\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n@keyframes zoomOutUp {\n  40% {\n    opacity: 1;\n    -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);\n    -webkit-transform-origin: center bottom;\n    transform-origin: center bottom;\n    -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n\n\n.zoomOutUp {\n  -webkit-animation-name: zoomOutUp;\n  animation-name: zoomOutUp;\n}\n\n@-webkit-keyframes slideInDown {\n  from {\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n@keyframes slideInDown {\n  from {\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n.slideInDown {\n  -webkit-animation-name: slideInDown;\n  animation-name: slideInDown;\n}\n\n@-webkit-keyframes slideInLeft {\n  from {\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n@keyframes slideInLeft {\n  from {\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n.slideInLeft {\n  -webkit-animation-name: slideInLeft;\n  animation-name: slideInLeft;\n}\n\n@-webkit-keyframes slideInRight {\n  from {\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n@keyframes slideInRight {\n  from {\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n.slideInRight {\n  -webkit-animation-name: slideInRight;\n  animation-name: slideInRight;\n}\n\n@-webkit-keyframes slideInUp {\n  from {\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n@keyframes slideInUp {\n  from {\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n\n.slideInUp {\n  -webkit-animation-name: slideInUp;\n  animation-name: slideInUp;\n}\n\n@-webkit-keyframes slideOutDown {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n  }\n}\n\n\n@keyframes slideOutDown {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(0, 100%, 0);\n    transform: translate3d(0, 100%, 0);\n  }\n}\n\n\n.slideOutDown {\n  -webkit-animation-name: slideOutDown;\n  animation-name: slideOutDown;\n}\n\n@-webkit-keyframes slideOutLeft {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n  }\n}\n\n\n@keyframes slideOutLeft {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n  }\n}\n\n\n.slideOutLeft {\n  -webkit-animation-name: slideOutLeft;\n  animation-name: slideOutLeft;\n}\n\n@-webkit-keyframes slideOutRight {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n  }\n}\n\n\n@keyframes slideOutRight {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n  }\n}\n\n\n.slideOutRight {\n  -webkit-animation-name: slideOutRight;\n  animation-name: slideOutRight;\n}\n\n@-webkit-keyframes slideOutUp {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n  }\n}\n\n\n@keyframes slideOutUp {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    -webkit-transform: translate3d(0, -100%, 0);\n    transform: translate3d(0, -100%, 0);\n  }\n}\n\n\n.slideOutUp {\n  -webkit-animation-name: slideOutUp;\n  animation-name: slideOutUp;\n}\n\n@keyframes spin {\n  from {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: rotate3d(0, 0, 1, -360deg);\n    transform: rotate3d(0, 0, 1, -360deg);\n  }\n\n  to {\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    -webkit-transform: none;\n    transform: none;\n  }\n}\n\n\n.spin {\n  -webkit-animation-name: spin;\n  animation-name: spin;\n}"
  },
  {
    "path": "client/libs/codemirror-merge/diff-match-patch.js",
    "content": "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]};\ndiff_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,\nb.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};\ndiff_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,\na),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&&100<a.length&&100<b.length?this.diff_lineMode_(a,b,d):this.diff_bisect_(a,b,d)};\ndiff_match_patch.prototype.diff_lineMode_=function(a,b,c){var d=this.diff_linesToChars_(a,b);a=d.chars1;b=d.chars2;d=d.lineArray;a=this.diff_main(a,b,!1,c);this.diff_charsToLines_(a,d);this.diff_cleanupSemantic(a);a.push(new diff_match_patch.Diff(DIFF_EQUAL,\"\"));for(var e=d=b=0,f=\"\",g=\"\";b<a.length;){switch(a[b][0]){case DIFF_INSERT:e++;g+=a[b][1];break;case DIFF_DELETE:d++;f+=a[b][1];break;case DIFF_EQUAL:if(1<=d&&1<=e){a.splice(b-d-e,d+e);b=b-d-e;d=this.diff_main(f,g,!1,c);for(e=d.length-1;0<=e;e--)a.splice(b,\n0,d[e]);b+=d.length}d=e=0;g=f=\"\"}b++}a.pop();return a};\ndiff_match_patch.prototype.diff_bisect_=function(a,b,c){for(var d=a.length,e=b.length,f=Math.ceil((d+e)/2),g=2*f,h=Array(g),l=Array(g),k=0;k<g;k++)h[k]=-1,l[k]=-1;h[f+1]=0;l[f+1]=0;k=d-e;for(var m=0!=k%2,p=0,x=0,w=0,q=0,t=0;t<f&&!((new Date).getTime()>c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]<h[n+1]?h[n+1]:h[n-1]+1;for(var y=r-v;r<d&&y<e&&a.charAt(r)==b.charAt(y);)r++,y++;h[n]=r;if(r>d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n<g&&-1!=l[n])){var u=d-l[n];if(r>=\nu)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]<l[n+1]?l[n+1]:l[n-1]+1;for(r=u-v;u<d&&r<e&&a.charAt(d-u-1)==b.charAt(e-r-1);)u++,r++;l[n]=u;if(u>d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n<g&&-1!=h[n]&&(r=h[n],y=f+r-n,u=d-u,r>=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)]};\ndiff_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)};\ndiff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b=\"\",c=0,g=-1,h=d.length;g<a.length-1;){g=a.indexOf(\"\\n\",c);-1==g&&(g=a.length-1);var l=a.substring(c,g+1);(e.hasOwnProperty?e.hasOwnProperty(l):void 0!==e[l])?b+=String.fromCharCode(e[l]):(h==f&&(l=a.substring(c),g=a.length),b+=String.fromCharCode(h),e[l]=h,d[h++]=l);c=g+1}return b}var d=[],e={};d[0]=\"\";var f=4E4,g=c(a);f=65535;var h=c(b);return{chars1:g,chars2:h,lineArray:d}};\ndiff_match_patch.prototype.diff_charsToLines_=function(a,b){for(var c=0;c<a.length;c++){for(var d=a[c][1],e=[],f=0;f<d.length;f++)e[f]=b[d.charCodeAt(f)];a[c][1]=e.join(\"\")}};diff_match_patch.prototype.diff_commonPrefix=function(a,b){if(!a||!b||a.charAt(0)!=b.charAt(0))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;)a.substring(f,e)==b.substring(f,e)?f=c=e:d=e,e=Math.floor((d-c)/2+c);return e};\ndiff_match_patch.prototype.diff_commonSuffix=function(a,b){if(!a||!b||a.charAt(a.length-1)!=b.charAt(b.length-1))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;)a.substring(a.length-e,a.length-f)==b.substring(b.length-e,b.length-f)?f=c=e:d=e,e=Math.floor((d-c)/2+c);return e};\ndiff_match_patch.prototype.diff_commonOverlap_=function(a,b){var c=a.length,d=b.length;if(0==c||0==d)return 0;c>d?a=a.substring(c-d):c<d&&(b=b.substring(0,c));c=Math.min(c,d);if(a==b)return c;d=0;for(var e=1;;){var f=a.substring(c-e);f=b.indexOf(f);if(-1==f)return d;e+=f;if(0==f||a.substring(c-e)==b.substring(0,e))d=e,e++}};\ndiff_match_patch.prototype.diff_halfMatch_=function(a,b){function c(a,b,c){for(var d=a.substring(c,c+Math.floor(a.length/4)),e=-1,g=\"\",h,k,l,m;-1!=(e=b.indexOf(d,e+1));){var p=f.diff_commonPrefix(a.substring(c),b.substring(e)),u=f.diff_commonSuffix(a.substring(0,c),b.substring(0,e));g.length<u+p&&(g=b.substring(e-u,e)+b.substring(e,e+p),h=a.substring(0,c-u),k=a.substring(c+p),l=b.substring(0,e-u),m=b.substring(e+p))}return 2*g.length>=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null;\nvar d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.length<d.length)return null;var f=this,g=c(d,e,Math.ceil(d.length/4));d=c(d,e,Math.ceil(d.length/2));if(g||d)g=d?g?g[4].length>d[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]]};\ndiff_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<a.length;)a[f][0]==DIFF_EQUAL?(c[d++]=f,g=l,h=k,k=l=0,e=a[f][1]):(a[f][0]==DIFF_INSERT?l+=a[f][1].length:k+=a[f][1].length,e&&e.length<=Math.max(g,h)&&e.length<=Math.max(l,k)&&(a.splice(c[d-1],0,new diff_match_patch.Diff(DIFF_DELETE,e)),a[c[d-1]+1][0]=DIFF_INSERT,d--,d--,f=0<d?c[d-1]:-1,k=l=h=g=0,e=null,b=!0)),f++;b&&this.diff_cleanupMerge(a);this.diff_cleanupSemanticLossless(a);for(f=1;f<\na.length;){if(a[f-1][0]==DIFF_DELETE&&a[f][0]==DIFF_INSERT){b=a[f-1][1];c=a[f][1];d=this.diff_commonOverlap_(b,c);e=this.diff_commonOverlap_(c,b);if(d>=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,\na[f+1][1]=b.substring(e),f++;f++}f++}};\ndiff_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_);\nreturn k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c<a.length-1;){if(a[c-1][0]==DIFF_EQUAL&&a[c+1][0]==DIFF_EQUAL){var d=a[c-1][1],e=a[c][1],f=a[c+1][1],g=this.diff_commonSuffix(d,e);if(g){var h=e.substring(e.length-g);d=d.substring(0,d.length-g);e=h+e.substring(0,e.length-g);f=h+f}g=d;h=e;for(var l=f,k=b(d,e)+b(e,f);e.charAt(0)===f.charAt(0);){d+=e.charAt(0);e=e.substring(1)+f.charAt(0);f=f.substring(1);var m=b(d,e)+b(e,f);m>=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-\n1,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/;\ndiff_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;f<a.length;)a[f][0]==DIFF_EQUAL?(a[f][1].length<this.Diff_EditCost&&(l||k)?(c[d++]=f,g=l,h=k,e=a[f][1]):(d=0,e=null),l=k=!1):(a[f][0]==DIFF_DELETE?k=!0:l=!0,e&&(g&&h&&l&&k||e.length<this.Diff_EditCost/2&&3==g+h+l+k)&&(a.splice(c[d-1],0,new diff_match_patch.Diff(DIFF_DELETE,e)),a[c[d-1]+1][0]=DIFF_INSERT,d--,e=null,g&&h?(l=k=!0,d=0):(d--,f=0<d?c[d-1]:-1,l=k=!1),b=!0)),f++;b&&this.diff_cleanupMerge(a)};\ndiff_match_patch.prototype.diff_cleanupMerge=function(a){a.push(new diff_match_patch.Diff(DIFF_EQUAL,\"\"));for(var b=0,c=0,d=0,e=\"\",f=\"\",g;b<a.length;)switch(a[b][0]){case DIFF_INSERT:d++;f+=a[b][1];b++;break;case DIFF_DELETE:c++;e+=a[b][1];b++;break;case DIFF_EQUAL:1<c+d?(0!==c&&0!==d&&(g=this.diff_commonPrefix(f,e),0!==g&&(0<b-c-d&&a[b-c-d-1][0]==DIFF_EQUAL?a[b-c-d-1][1]+=f.substring(0,g):(a.splice(0,0,new diff_match_patch.Diff(DIFF_EQUAL,f.substring(0,g))),b++),f=f.substring(g),e=e.substring(g)),\ng=this.diff_commonSuffix(f,e),0!==g&&(a[b][1]=f.substring(f.length-g)+a[b][1],f=f.substring(0,f.length-g),e=e.substring(0,e.length-g))),b-=c+d,a.splice(b,c+d),e.length&&(a.splice(b,0,new diff_match_patch.Diff(DIFF_DELETE,e)),b++),f.length&&(a.splice(b,0,new diff_match_patch.Diff(DIFF_INSERT,f)),b++),b++):0!==b&&a[b-1][0]==DIFF_EQUAL?(a[b-1][1]+=a[b][1],a.splice(b,1)):b++,c=d=0,f=e=\"\"}\"\"===a[a.length-1][1]&&a.pop();c=!1;for(b=1;b<a.length-1;)a[b-1][0]==DIFF_EQUAL&&a[b+1][0]==DIFF_EQUAL&&(a[b][1].substring(a[b][1].length-\na[b-1][1].length)==a[b-1][1]?(a[b][1]=a[b-1][1]+a[b][1].substring(0,a[b][1].length-a[b-1][1].length),a[b+1][1]=a[b-1][1]+a[b+1][1],a.splice(b-1,1),c=!0):a[b][1].substring(0,a[b+1][1].length)==a[b+1][1]&&(a[b-1][1]+=a[b+1][1],a[b][1]=a[b][1].substring(a[b+1][1].length)+a[b+1][1],a.splice(b+1,1),c=!0)),b++;c&&this.diff_cleanupMerge(a)};\ndiff_match_patch.prototype.diff_xIndex=function(a,b){var c=0,d=0,e=0,f=0,g;for(g=0;g<a.length;g++){a[g][0]!==DIFF_INSERT&&(c+=a[g][1].length);a[g][0]!==DIFF_DELETE&&(d+=a[g][1].length);if(c>b)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)};\ndiff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=/</g,e=/>/g,f=/\\n/g,g=0;g<a.length;g++){var h=a[g][0],l=a[g][1].replace(c,\"&amp;\").replace(d,\"&lt;\").replace(e,\"&gt;\").replace(f,\"&para;<br>\");switch(h){case DIFF_INSERT:b[g]='<ins style=\"background:#e6ffe6;\">'+l+\"</ins>\";break;case DIFF_DELETE:b[g]='<del style=\"background:#ffe6e6;\">'+l+\"</del>\";break;case DIFF_EQUAL:b[g]=\"<span>\"+l+\"</span>\"}}return b.join(\"\")};\ndiff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;c<a.length;c++)a[c][0]!==DIFF_INSERT&&(b[c]=a[c][1]);return b.join(\"\")};diff_match_patch.prototype.diff_text2=function(a){for(var b=[],c=0;c<a.length;c++)a[c][0]!==DIFF_DELETE&&(b[c]=a[c][1]);return b.join(\"\")};\ndiff_match_patch.prototype.diff_levenshtein=function(a){for(var b=0,c=0,d=0,e=0;e<a.length;e++){var f=a[e][1];switch(a[e][0]){case DIFF_INSERT:c+=f.length;break;case DIFF_DELETE:d+=f.length;break;case DIFF_EQUAL:b+=Math.max(c,d),d=c=0}}return b+=Math.max(c,d)};\ndiff_match_patch.prototype.diff_toDelta=function(a){for(var b=[],c=0;c<a.length;c++)switch(a[c][0]){case DIFF_INSERT:b[c]=\"+\"+encodeURI(a[c][1]);break;case DIFF_DELETE:b[c]=\"-\"+a[c][1].length;break;case DIFF_EQUAL:b[c]=\"=\"+a[c][1].length}return b.join(\"\\t\").replace(/%20/g,\" \")};\ndiff_match_patch.prototype.diff_fromDelta=function(a,b){for(var c=[],d=0,e=0,f=b.split(/\\t/g),g=0;g<f.length;g++){var h=f[g].substring(1);switch(f[g].charAt(0)){case \"+\":try{c[d++]=new diff_match_patch.Diff(DIFF_INSERT,decodeURI(h))}catch(k){throw Error(\"Illegal escape in diff_fromDelta: \"+h);}break;case \"-\":case \"=\":var l=parseInt(h,10);if(isNaN(l)||0>l)throw Error(\"Invalid number in diff_fromDelta: \"+h);h=a.substring(e,e+=l);\"=\"==f[g].charAt(0)?c[d++]=new diff_match_patch.Diff(DIFF_EQUAL,h):c[d++]=\nnew diff_match_patch.Diff(DIFF_DELETE,h);break;default:if(f[g])throw Error(\"Invalid diff operation in diff_fromDelta: \"+f[g]);}}if(e!=a.length)throw Error(\"Delta length (\"+e+\") does not equal source text length (\"+a.length+\").\");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error(\"Null input. (match_main)\");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};\ndiff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return f.Match_Distance?e+g/f.Match_Distance:g?1:e}if(b.length>this.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<<b.length-1;h=-1;for(var k,m,p=b.length+a.length,x,w=0;w<b.length;w++){k=0;for(m=p;k<m;)d(w,\nc+m)<=g?k=m:p=m,m=Math.floor((p-k)/2+k);p=m;k=Math.max(1,c-m+1);var q=Math.min(c+m,a.length)+b.length;m=Array(q+2);for(m[q+1]=(1<<w)-1;q>=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};\ndiff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c<a.length;c++)b[a.charAt(c)]=0;for(c=0;c<a.length;c++)b[a.charAt(c)]|=1<<a.length-c-1;return b};\ndiff_match_patch.prototype.patch_addContext_=function(a,b){if(0!=b.length){if(null===a.start2)throw Error(\"patch not initialized\");for(var c=b.substring(a.start2,a.start2+a.length1),d=0;b.indexOf(c)!=b.lastIndexOf(c)&&c.length<this.Match_MaxBits-this.Patch_Margin-this.Patch_Margin;)d+=this.Patch_Margin,c=b.substring(a.start2-d,a.start2+a.length1+d);d+=this.Patch_Margin;(c=b.substring(a.start2-d,a.start2))&&a.diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));(d=b.substring(a.start2+a.length1,\na.start2+a.length1+d))&&a.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,d));a.start1-=c.length;a.start2-=c.length;a.length1+=c.length+d.length;a.length2+=c.length+d.length}};\ndiff_match_patch.prototype.patch_make=function(a,b,c){if(\"string\"==typeof a&&\"string\"==typeof b&&\"undefined\"==typeof c){var d=a;b=this.diff_main(d,b,!0);2<b.length&&(this.diff_cleanupSemantic(b),this.diff_cleanupEfficiency(b))}else if(a&&\"object\"==typeof a&&\"undefined\"==typeof b&&\"undefined\"==typeof c)b=a,d=this.diff_text1(b);else if(\"string\"==typeof a&&b&&\"object\"==typeof b&&\"undefined\"==typeof c)d=a;else if(\"string\"==typeof a&&\"string\"==typeof b&&c&&\"object\"==typeof c)d=a,b=c;else throw Error(\"Unknown call format to patch_make.\");\nif(0===b.length)return[];c=[];a=new diff_match_patch.patch_obj;for(var e=0,f=0,g=0,h=d,l=0;l<b.length;l++){var k=b[l][0],m=b[l][1];e||k===DIFF_EQUAL||(a.start1=f,a.start2=g);switch(k){case DIFF_INSERT:a.diffs[e++]=b[l];a.length2+=m.length;d=d.substring(0,g)+m+d.substring(g);break;case DIFF_DELETE:a.length1+=m.length;a.diffs[e++]=b[l];d=d.substring(0,g)+d.substring(g+m.length);break;case DIFF_EQUAL:m.length<=2*this.Patch_Margin&&e&&b.length!=l+1?(a.diffs[e++]=b[l],a.length1+=m.length,a.length2+=m.length):\nm.length>=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};\ndiff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;c<a.length;c++){var d=a[c],e=new diff_match_patch.patch_obj;e.diffs=[];for(var f=0;f<d.diffs.length;f++)e.diffs[f]=new diff_match_patch.Diff(d.diffs[f][0],d.diffs[f][1]);e.start1=d.start1;e.start2=d.start2;e.length1=d.length1;e.length2=d.length2;b[c]=e}return b};\ndiff_match_patch.prototype.patch_apply=function(a,b){if(0==a.length)return[b,[]];a=this.patch_deepCopy(a);var c=this.patch_addPadding(a);b=c+b+c;this.patch_splitMax(a);for(var d=0,e=[],f=0;f<a.length;f++){var g=a[f].start2+d,h=this.diff_text1(a[f].diffs),l=-1;if(h.length>this.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,\ng);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;l<a[f].diffs.length;l++){var p=a[f].diffs[l];p[0]!==DIFF_EQUAL&&(m=this.diff_xIndex(g,h));p[0]===\nDIFF_INSERT?b=b.substring(0,k+m)+p[1]+b.substring(k+m):p[0]===DIFF_DELETE&&(b=b.substring(0,k+m)+b.substring(k+this.diff_xIndex(g,h+p[1].length)));p[0]!==DIFF_DELETE&&(h+=p[1].length)}}}b=b.substring(c.length,b.length-c.length);return[b,e]};\ndiff_match_patch.prototype.patch_addPadding=function(a){for(var b=this.Patch_Margin,c=\"\",d=1;d<=b;d++)c+=String.fromCharCode(d);for(d=0;d<a.length;d++)a[d].start1+=b,a[d].start2+=b;d=a[0];var e=d.diffs;if(0==e.length||e[0][0]!=DIFF_EQUAL)e.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.start1-=b,d.start2-=b,d.length1+=b,d.length2+=b;else if(b>e[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;\n0==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};\ndiff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c<a.length;c++)if(!(a[c].length1<=b)){var d=a[c];a.splice(c--,1);for(var e=d.start1,f=d.start2,g=\"\";0!==d.diffs.length;){var h=new diff_match_patch.patch_obj,l=!0;h.start1=e-g.length;h.start2=f-g.length;\"\"!==g&&(h.length1=h.length2=g.length,h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,g)));for(;0!==d.diffs.length&&h.length1<b-this.Patch_Margin;){g=d.diffs[0][0];var k=d.diffs[0][1];g===DIFF_INSERT?(h.length2+=\nk.length,f+=k.length,h.diffs.push(d.diffs.shift()),l=!1):g===DIFF_DELETE&&1==h.diffs.length&&h.diffs[0][0]==DIFF_EQUAL&&k.length>2*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);\ng=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<a.length;c++)b[c]=a[c];return b.join(\"\")};\ndiff_match_patch.prototype.patch_fromText=function(a){var b=[];if(!a)return b;a=a.split(\"\\n\");for(var c=0,d=/^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$/;c<a.length;){var e=a[c].match(d);if(!e)throw Error(\"Invalid patch string: \"+a[c]);var f=new diff_match_patch.patch_obj;b.push(f);f.start1=parseInt(e[1],10);\"\"===e[2]?(f.start1--,f.length1=1):\"0\"==e[2]?f.length1=0:(f.start1--,f.length1=parseInt(e[2],10));f.start2=parseInt(e[3],10);\"\"===e[4]?(f.start2--,f.length2=1):\"0\"==e[4]?f.length2=0:(f.start2--,f.length2=\nparseInt(e[4],10));for(c++;c<a.length;){e=a[c].charAt(0);try{var g=decodeURI(a[c].substring(1))}catch(h){throw Error(\"Illegal escape in patch_fromText: \"+g);}if(\"-\"==e)f.diffs.push(new diff_match_patch.Diff(DIFF_DELETE,g));else if(\"+\"==e)f.diffs.push(new diff_match_patch.Diff(DIFF_INSERT,g));else if(\" \"==e)f.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,g));else if(\"@\"==e)break;else if(\"\"!==e)throw Error('Invalid patch mode \"'+e+'\" in: '+g);c++}}return b};\ndiff_match_patch.patch_obj=function(){this.diffs=[];this.start2=this.start1=null;this.length2=this.length1=0};\ndiff_match_patch.patch_obj.prototype.toString=function(){for(var a=[\"@@ -\"+(0===this.length1?this.start1+\",0\":1==this.length1?this.start1+1:this.start1+1+\",\"+this.length1)+\" +\"+(0===this.length2?this.start2+\",0\":1==this.length2?this.start2+1:this.start2+1+\",\"+this.length2)+\" @@\\n\"],b,c=0;c<this.diffs.length;c++){switch(this.diffs[c][0]){case DIFF_INSERT:b=\"+\";break;case DIFF_DELETE:b=\"-\";break;case DIFF_EQUAL:b=\" \"}a[c+1]=b+encodeURI(this.diffs[c][1])+\"\\n\"}return a.join(\"\").replace(/%20/g,\" \")};\nthis.diff_match_patch=diff_match_patch;this.DIFF_DELETE=DIFF_DELETE;this.DIFF_INSERT=DIFF_INSERT;this.DIFF_EQUAL=DIFF_EQUAL;\n\nwindow.diff_match_patch = diff_match_patch\nwindow.DIFF_INSERT = DIFF_INSERT\nwindow.DIFF_DELETE = DIFF_DELETE\nwindow.DIFF_EQUAL = DIFF_EQUAL\n"
  },
  {
    "path": "client/libs/markdown-it-underline/index.js",
    "content": "const renderEm = (tokens, idx, opts, env, slf) => {\n  const token = tokens[idx];\n  if (token.markup === '_') {\n    token.tag = 'u';\n  }\n  return slf.renderToken(tokens, idx, opts);\n}\n\nmodule.exports = (md) => {\n  md.renderer.rules.em_open = renderEm;\n  md.renderer.rules.em_close = renderEm;\n}\n"
  },
  {
    "path": "client/libs/modernizr/modernizr.js",
    "content": "/*! modernizr 3.6.0 (Custom Build) | MIT *\n * https://modernizr.com/download/?-setclasses !*/\n!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<e.options.aliases.length;s++)n.push(e.options.aliases[s].toLowerCase());for(o=a(e.fn,\"function\")?e.fn():e.fn,i=0;i<n.length;i++)l=n[i],r=l.split(\".\"),1===r.length?Modernizr[r[0]]=o:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=o),t.push((o?\"\":\"no-\")+r.join(\"-\"))}}var t=[],f=[],l={_version:\"3.6.0\",_config:{classPrefix:\"\",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(n,e){var s=this;setTimeout(function(){e(s[n])},0)},addTest:function(n,e,s){f.push({name:n,fn:e,options:s})},addAsyncTest:function(n){f.push({name:null,fn:n})}},Modernizr=function(){};Modernizr.prototype=l,Modernizr=new Modernizr;var r=e.documentElement,c=\"svg\"===r.nodeName.toLowerCase();i(),o(t),delete l.addTest,delete l.addAsyncTest;for(var u=0;u<Modernizr._q.length;u++)Modernizr._q[u]();n.Modernizr=Modernizr}(window,document);"
  },
  {
    "path": "client/libs/prism/prism.css",
    "content": "/* PrismJS 1.11.0\nhttp://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 */\n/**\n * prism.js Dark theme for JavaScript, CSS and HTML\n * Based on the slides of the talk “/Reg(exp){2}lained/”\n * @author Lea Verou\n */\n\ncode[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tcolor: white;\n\tbackground: none;\n\ttext-shadow: 0 -.1em .2em black;\n\tfont-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\n\ttext-align: left;\n\twhite-space: pre;\n\tword-spacing: normal;\n\tword-break: normal;\n\tword-wrap: normal;\n\tline-height: 1.5;\n\n\t-moz-tab-size: 4;\n\t-o-tab-size: 4;\n\ttab-size: 4;\n\n\t-webkit-hyphens: none;\n\t-moz-hyphens: none;\n\t-ms-hyphens: none;\n\thyphens: none;\n}\n\n@media print {\n\tcode[class*=\"language-\"],\n\tpre[class*=\"language-\"] {\n\t\ttext-shadow: none;\n\t}\n}\n\npre[class*=\"language-\"],\n:not(pre) > code[class*=\"language-\"] {\n\tbackground: hsl(30, 20%, 25%);\n}\n\n/* Code blocks */\npre[class*=\"language-\"] {\n\tpadding: 1em;\n\tmargin: .5em 0;\n\toverflow: auto;\n\tborder: .3em solid hsl(30, 20%, 40%);\n\tborder-radius: .5em;\n\tbox-shadow: 1px 1px .5em black inset;\n}\n\n/* Inline code */\n:not(pre) > code[class*=\"language-\"] {\n\tpadding: .15em .2em .05em;\n\tborder-radius: .3em;\n\tborder: .13em solid hsl(30, 20%, 40%);\n\tbox-shadow: 1px 1px .3em -.1em black inset;\n\twhite-space: normal;\n}\n\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n\tcolor: hsl(30, 20%, 50%);\n}\n\n.token.punctuation {\n\topacity: .7;\n}\n\n.namespace {\n\topacity: .7;\n}\n\n.token.property,\n.token.tag,\n.token.boolean,\n.token.number,\n.token.constant,\n.token.symbol {\n\tcolor: hsl(350, 40%, 70%);\n}\n\n.token.selector,\n.token.attr-name,\n.token.string,\n.token.char,\n.token.builtin,\n.token.inserted {\n\tcolor: hsl(75, 70%, 60%);\n}\n\n.token.operator,\n.token.entity,\n.token.url,\n.language-css .token.string,\n.style .token.string,\n.token.variable {\n\tcolor: hsl(40, 90%, 60%);\n}\n\n.token.atrule,\n.token.attr-value,\n.token.keyword {\n\tcolor: hsl(350, 40%, 70%);\n}\n\n.token.regex,\n.token.important {\n\tcolor: #e90;\n}\n\n.token.important,\n.token.bold {\n\tfont-weight: bold;\n}\n.token.italic {\n\tfont-style: italic;\n}\n\n.token.entity {\n\tcursor: help;\n}\n\n.token.deleted {\n\tcolor: red;\n}\n\npre.line-numbers {\n\tposition: relative;\n\tpadding-left: 3.8em;\n\tcounter-reset: linenumber;\n}\n\npre.line-numbers > code {\n\tposition: relative;\n    white-space: inherit;\n}\n\n.line-numbers .line-numbers-rows {\n\tposition: absolute;\n\tpointer-events: none;\n\ttop: 0;\n\tfont-size: 100%;\n\tleft: -3.8em;\n\twidth: 3em; /* works for line-numbers below 1000 lines */\n\tletter-spacing: -1px;\n\tborder-right: 1px solid #999;\n\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n\n}\n\n\t.line-numbers-rows > span {\n\t\tpointer-events: none;\n\t\tdisplay: block;\n\t\tcounter-increment: linenumber;\n\t}\n\n\t\t.line-numbers-rows > span:before {\n\t\t\tcontent: counter(linenumber);\n\t\t\tcolor: #999;\n\t\t\tdisplay: block;\n\t\t\tpadding-right: 0.8em;\n\t\t\ttext-align: right;\n\t\t}\n"
  },
  {
    "path": "client/libs/prism/prism.js",
    "content": "/* PrismJS 1.11.0\nhttp://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 */\nvar _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,\"&amp;\").replace(/</g,\"&lt;\").replace(/\\u00a0/g,\" \")},type:function(e){return Object.prototype.toString.call(e).match(/\\[object (\\w+)\\]/)[1]},objId:function(e){return e.__id||Object.defineProperty(e,\"__id\",{value:++t}),e.__id},clone:function(e){var t=n.util.type(e);switch(t){case\"Object\":var r={};for(var a in e)e.hasOwnProperty(a)&&(r[a]=n.util.clone(e[a]));return r;case\"Array\":return e.map(function(e){return n.util.clone(e)})}return e}},languages:{extend:function(e,t){var r=n.util.clone(n.languages[e]);for(var a in t)r[a]=t[a];return r},insertBefore:function(e,t,r,a){a=a||n.languages;var l=a[e];if(2==arguments.length){r=arguments[1];for(var i in r)r.hasOwnProperty(i)&&(l[i]=r[i]);return l}var o={};for(var s in l)if(l.hasOwnProperty(s)){if(s==t)for(var i in r)r.hasOwnProperty(i)&&(o[i]=r[i]);o[s]=l[s]}return n.languages.DFS(n.languages,function(t,n){n===a[e]&&t!=e&&(this[t]=o)}),a[e]=o},DFS:function(e,t,r,a){a=a||{};for(var l in e)e.hasOwnProperty(l)&&(t.call(e,l,e[l],r||l),\"Object\"!==n.util.type(e[l])||a[n.util.objId(e[l])]?\"Array\"!==n.util.type(e[l])||a[n.util.objId(e[l])]||(a[n.util.objId(e[l])]=!0,n.languages.DFS(e[l],t,l,a)):(a[n.util.objId(e[l])]=!0,n.languages.DFS(e[l],t,null,a)))}},plugins:{},highlightAll:function(e,t){n.highlightAllUnder(document,e,t)},highlightAllUnder:function(e,t,r){var a={callback:r,selector:'code[class*=\"language-\"], [class*=\"language-\"] code, code[class*=\"lang-\"], [class*=\"lang-\"] code'};n.hooks.run(\"before-highlightall\",a);for(var l,i=a.elements||e.querySelectorAll(a.selector),o=0;l=i[o++];)n.highlightElement(l,t===!0,a.callback)},highlightElement:function(t,r,a){for(var l,i,o=t;o&&!e.test(o.className);)o=o.parentNode;o&&(l=(o.className.match(e)||[,\"\"])[1].toLowerCase(),i=n.languages[l]),t.className=t.className.replace(e,\"\").replace(/\\s+/g,\" \")+\" language-\"+l,t.parentNode&&(o=t.parentNode,/pre/i.test(o.nodeName)&&(o.className=o.className.replace(e,\"\").replace(/\\s+/g,\" \")+\" language-\"+l));var s=t.textContent,g={element:t,language:l,grammar:i,code:s};if(n.hooks.run(\"before-sanity-check\",g),!g.code||!g.grammar)return g.code&&(n.hooks.run(\"before-highlight\",g),g.element.textContent=g.code,n.hooks.run(\"after-highlight\",g)),n.hooks.run(\"complete\",g),void 0;if(n.hooks.run(\"before-highlight\",g),r&&_self.Worker){var u=new Worker(n.filename);u.onmessage=function(e){g.highlightedCode=e.data,n.hooks.run(\"before-insert\",g),g.element.innerHTML=g.highlightedCode,a&&a.call(g.element),n.hooks.run(\"after-highlight\",g),n.hooks.run(\"complete\",g)},u.postMessage(JSON.stringify({language:g.language,code:g.code,immediateClose:!0}))}else g.highlightedCode=n.highlight(g.code,g.grammar,g.language),n.hooks.run(\"before-insert\",g),g.element.innerHTML=g.highlightedCode,a&&a.call(t),n.hooks.run(\"after-highlight\",g),n.hooks.run(\"complete\",g)},highlight:function(e,t,a){var l=n.tokenize(e,t);return r.stringify(n.util.encode(l),a)},matchGrammar:function(e,t,r,a,l,i,o){var s=n.Token;for(var g in r)if(r.hasOwnProperty(g)&&r[g]){if(g==o)return;var u=r[g];u=\"Array\"===n.util.type(u)?u:[u];for(var c=0;c<u.length;++c){var h=u[c],f=h.inside,d=!!h.lookbehind,m=!!h.greedy,p=0,y=h.alias;if(m&&!h.pattern.global){var v=h.pattern.toString().match(/[imuy]*$/)[0];h.pattern=RegExp(h.pattern.source,v+\"g\")}h=h.pattern||h;for(var b=a,k=l;b<t.length;k+=t[b].length,++b){var w=t[b];if(t.length>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,\"&quot;\")+'\"'}).join(\" \");return\"<\"+l.tag+' class=\"'+l.classes.join(\" \")+'\"'+(o?\" \"+o:\"\")+\">\"+l.content+\"</\"+l.tag+\">\"},!_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);\nPrism.languages.markup={comment:/<!--[\\s\\S]*?-->/,prolog:/<\\?[\\s\\S]+?\\?>/,doctype:/<!DOCTYPE[\\s\\S]+?>/i,cdata:/<!\\[CDATA\\[[\\s\\S]*?]]>/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(/&amp;/,\"&\"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup;\nPrism.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:/(<style[\\s\\S]*?>)[\\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));\nPrism.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:/[{}[\\];(),.:]/};\nPrism.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:/(<script[\\s\\S]*?>)[\\s\\S]*?(?=<\\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:\"language-javascript\",greedy:!0}}),Prism.languages.js=Prism.languages.javascript;\nPrism.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\"];\n!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);\nPrism.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:/[,;:()]/};\nPrism.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}});\nPrism.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\"}}}});\nPrism.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/});\n!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);\nPrism.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)}}}});\nPrism.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\"}}}});\nPrism.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\"];\nPrism.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:/[!(){}\\[\\]:=,]/};\n!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<t.length;++n){var r=t[n],o=a.tokenStack[r];a.highlightedCode=a.highlightedCode.replace(\"___HANDLEBARS\"+r+\"___\",e.highlight(o,a.grammar,\"handlebars\").replace(/\\$/g,\"$$$$\"))}a.element.innerHTML=a.highlightedCode}})}(Prism);\nPrism.languages.haskell={comment:{pattern:/(^|[^-!#$%*+=?&@|~.:<>^\\\\\\/])(?:--[^-!#$%*+=?&@|~.:<>^\\\\\\/].*|{-[\\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:/[{}[\\];(),.:]/};\nPrism.languages.ini={comment:/^[ \\t]*;.*$/m,selector:/^[ \\t]*\\[.*?\\]/m,constant:/^[ \\t]*[^\\s=]+?(?=[ \\t]*=)/m,\"attr-value\":{pattern:/=.*/,inside:{punctuation:/^[=]/}}};\nPrism.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}});\nPrism.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;\n!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);\n!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);\nPrism.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\"}});\nPrism.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:/[:;(){}]/};\nPrism.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);\nPrism.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}|[.,;\\[\\](){}!]/};\nPrism.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});\nPrism.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:/-[->]?|\\+\\+?|!=?|<<?=?|>>?=?|==?|&&?|\\|\\|?|[~^%?*\\/@]/});\nPrism.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:/[{}[\\];(),:]/};\nPrism.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<n.length;++a){var t=n[a],r=e.tokenStack[t];e.highlightedCode=e.highlightedCode.replace(\"___PHP\"+t+\"___\",'<span class=\"token php language-php\">'+Prism.highlight(r,e.grammar,\"php\").replace(/\\$/g,\"$$$$\")+\"</span>\")}e.element.innerHTML=e.highlightedCode}}));\nPrism.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);\n!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);\nPrism.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:/[{}[\\];(),.:]/};\nPrism.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;\nPrism.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:/[-+*\\/%!^=]=?|@|&[&=]?|\\|[|=]?|<<?=?|>>?=?/};\nPrism.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);\nPrism.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\"];\nPrism.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:/[.;:?\\[\\](){}]/};\nPrism.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:/[;[\\]()`,.]/};\n!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);\nPrism.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);\nPrism.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}]});\nPrism.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:/---|[:[\\]{}\\-,|>?]|\\.\\.\\./};\n!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(\"<span></span>\"),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]}}}}}();\n"
  },
  {
    "path": "client/libs/twemoji/twemoji-awesome.scss",
    "content": ".twa {\n  display: inline-block;\n  height: 1em;\n  width: 1em;\n  margin: 0 .05em 0 .1em;\n  vertical-align: -0.1em;\n  background-repeat: no-repeat;\n  background-position: center center;\n  background-size: 1em 1em;\n}\n\n$size-map: (\n  \"lg\": 1.33,\n  \"2x\": 2,\n  \"3x\": 3,\n  \"4x\": 4,\n  \"5x\": 5\n);\n\n@each $name, $size in $size-map {\n  .twa-#{$name} {\n    height: 1em * $size;\n    width: 1em * $size;\n    margin: 0 .05em * $size 0 .1em * $size;\n    vertical-align: -0.1em * $size;\n    background-size: 1em * $size 1em * $size;\n  }\n}\n\n$emoji-map: (\n  \"1f604\": \"smile\",\n  \"1f606\": \"laughing\",\n  \"1f60a\": \"blush\",\n  \"1f603\": \"smiley\",\n  \"263a\": \"relaxed\",\n  \"1f60f\": \"smirk\",\n  \"1f60d\": \"heart-eyes\",\n  \"1f618\": \"kissing-heart\",\n  \"1f61a\": \"kissing-closed-eyes\",\n  \"1f633\": \"flushed\",\n  \"1f625\": \"relieved\",\n  \"1f60c\": \"satisfied\",\n  \"1f601\": \"grin\",\n  \"1f609\": \"wink\",\n  \"1f61c\": \"stuck-out-tongue-winking-eye\",\n  \"1f61d\": \"stuck-out-tongue-closed-eyes\",\n  \"1f600\": \"grinning\",\n  \"1f617\": \"kissing\",\n  \"1f619\": \"kissing-smiling-eyes\",\n  \"1f61b\": \"stuck-out-tongue\",\n  \"1f634\": \"sleeping\",\n  \"1f61f\": \"worried\",\n  \"1f626\": \"frowning\",\n  \"1f627\": \"anguished\",\n  \"1f62e\": \"open-mouth\",\n  \"1f62c\": \"grimacing\",\n  \"1f615\": \"confused\",\n  \"1f62f\": \"hushed\",\n  \"1f611\": \"expressionless\",\n  \"1f612\": \"unamused\",\n  \"1f605\": \"sweat-smile\",\n  \"1f613\": \"sweat\",\n  \"1f629\": \"weary\",\n  \"1f614\": \"pensive\",\n  \"1f61e\": \"disappointed\",\n  \"1f616\": \"confounded\",\n  \"1f628\": \"fearful\",\n  \"1f630\": \"cold-sweat\",\n  \"1f623\": \"persevere\",\n  \"1f622\": \"cry\",\n  \"1f62d\": \"sob\",\n  \"1f602\": \"joy\",\n  \"1f632\": \"astonished\",\n  \"1f631\": \"scream\",\n  \"1f62b\": \"tired-face\",\n  \"1f620\": \"angry\",\n  \"1f621\": \"rage\",\n  \"1f624\": \"triumph\",\n  \"1f62a\": \"sleepy\",\n  \"1f60b\": \"yum\",\n  \"1f637\": \"mask\",\n  \"1f60e\": \"sunglasses\",\n  \"1f635\": \"dizzy-face\",\n  \"1f47f\": \"imp\",\n  \"1f608\": \"smiling-imp\",\n  \"1f610\": \"neutral-face\",\n  \"1f636\": \"no-mouth\",\n  \"1f607\": \"innocent\",\n  \"1f47d\": \"alien\",\n  \"1f49b\": \"yellow-heart\",\n  \"1f499\": \"blue-heart\",\n  \"1f49c\": \"purple-heart\",\n  \"2764\": \"heart\",\n  \"1f49a\": \"green-heart\",\n  \"1f494\": \"broken-heart\",\n  \"1f493\": \"heartbeat\",\n  \"1f497\": \"heartpulse\",\n  \"1f495\": \"two-hearts\",\n  \"1f49e\": \"revolving-hearts\",\n  \"1f498\": \"cupid\",\n  \"1f496\": \"sparkling-heart\",\n  \"2728\": \"sparkles\",\n  \"2b50\": \"star\",\n  \"1f31f\": \"star2\",\n  \"1f4ab\": \"dizzy\",\n  \"1f4a5\": \"boom\",\n  \"1f4a2\": \"anger\",\n  \"2757\": \"exclamation\",\n  \"2753\": \"question\",\n  \"2755\": \"grey-exclamation\",\n  \"2754\": \"grey-question\",\n  \"1f4a4\": \"zzz\",\n  \"1f4a8\": \"dash\",\n  \"1f4a6\": \"sweat-drops\",\n  \"1f3b6\": \"notes\",\n  \"1f3b5\": \"musical-note\",\n  \"1f525\": \"fire\",\n  \"1f4a9\": \"poop\",\n  \"1f44d\": \"thumbsup\",\n  \"1f44e\": \"thumbsdown\",\n  \"1f44c\": \"ok-hand\",\n  \"1f44a\": \"punch\",\n  \"270a\": \"fist\",\n  \"270c\": \"v\",\n  \"1f44b\": \"wave\",\n  \"270b\": \"hand\",\n  \"1f450\": \"open-hands\",\n  \"261d\": \"point-up\",\n  \"1f447\": \"point-down\",\n  \"1f448\": \"point-left\",\n  \"1f449\": \"point-right\",\n  \"1f64c\": \"raised-hands\",\n  \"1f64f\": \"pray\",\n  \"1f446\": \"point-up-2\",\n  \"1f44f\": \"clap\",\n  \"1f4aa\": \"muscle\",\n  \"1f6b6\": \"walking\",\n  \"1f3c3\": \"runner\",\n  \"1f46b\": \"couple\",\n  \"1f46a\": \"family\",\n  \"1f46c\": \"two-men-holding-hands\",\n  \"1f46d\": \"two-women-holding-hands\",\n  \"1f483\": \"dancer\",\n  \"1f46f\": \"dancers\",\n  \"1f646\": \"ok-woman\",\n  \"1f645\": \"no-good\",\n  \"1f481\": \"information-desk-person\",\n  \"1f64b\": \"raised-hand\",\n  \"1f470\": \"bride-with-veil\",\n  \"1f64e\": \"person-with-pouting-face\",\n  \"1f64d\": \"person-frowning\",\n  \"1f647\": \"bow\",\n  \"1f48f\": \"couplekiss\",\n  \"1f491\": \"couple-with-heart\",\n  \"1f486\": \"massage\",\n  \"1f487\": \"haircut\",\n  \"1f485\": \"nail-care\",\n  \"1f466\": \"boy\",\n  \"1f467\": \"girl\",\n  \"1f469\": \"woman\",\n  \"1f468\": \"man\",\n  \"1f476\": \"baby\",\n  \"1f475\": \"older-woman\",\n  \"1f474\": \"older-man\",\n  \"1f471\": \"person-with-blond-hair\",\n  \"1f472\": \"man-with-gua-pi-mao\",\n  \"1f473\": \"man-with-turban\",\n  \"1f477\": \"construction-worker\",\n  \"1f46e\": \"cop\",\n  \"1f47c\": \"angel\",\n  \"1f478\": \"princess\",\n  \"1f63a\": \"smiley-cat\",\n  \"1f638\": \"smile-cat\",\n  \"1f63b\": \"heart-eyes-cat\",\n  \"1f63d\": \"kissing-cat\",\n  \"1f63c\": \"smirk-cat\",\n  \"1f640\": \"scream-cat\",\n  \"1f63f\": \"crying-cat-face\",\n  \"1f639\": \"joy-cat\",\n  \"1f63e\": \"pouting-cat\",\n  \"1f479\": \"japanese-ogre\",\n  \"1f47a\": \"japanese-goblin\",\n  \"1f648\": \"see-no-evil\",\n  \"1f649\": \"hear-no-evil\",\n  \"1f64a\": \"speak-no-evil\",\n  \"1f482\": \"guardsman\",\n  \"1f480\": \"skull\",\n  \"1f463\": \"feet\",\n  \"1f444\": \"lips\",\n  \"1f48b\": \"kiss\",\n  \"1f4a7\": \"droplet\",\n  \"1f442\": \"ear\",\n  \"1f440\": \"eyes\",\n  \"1f443\": \"nose\",\n  \"1f445\": \"tongue\",\n  \"1f48c\": \"love-letter\",\n  \"1f464\": \"bust-in-silhouette\",\n  \"1f465\": \"busts-in-silhouette\",\n  \"1f4ac\": \"speech-balloon\",\n  \"1f4ad\": \"thought-balloon\",\n  \"2600\": \"sunny\",\n  \"2614\": \"umbrella\",\n  \"2601\": \"cloud\",\n  \"2744\": \"snowflake\",\n  \"26c4\": \"snowman\",\n  \"26a1\": \"zap\",\n  \"1f300\": \"cyclone\",\n  \"1f301\": \"foggy\",\n  \"1f30a\": \"ocean\",\n  \"1f431\": \"cat\",\n  \"1f436\": \"dog\",\n  \"1f42d\": \"mouse\",\n  \"1f439\": \"hamster\",\n  \"1f430\": \"rabbit\",\n  \"1f43a\": \"wolf\",\n  \"1f438\": \"frog\",\n  \"1f42f\": \"tiger\",\n  \"1f428\": \"koala\",\n  \"1f43b\": \"bear\",\n  \"1f437\": \"pig\",\n  \"1f43d\": \"pig-nose\",\n  \"1f42e\": \"cow\",\n  \"1f417\": \"boar\",\n  \"1f435\": \"monkey-face\",\n  \"1f412\": \"monkey\",\n  \"1f434\": \"horse\",\n  \"1f40e\": \"racehorse\",\n  \"1f42b\": \"camel\",\n  \"1f411\": \"sheep\",\n  \"1f418\": \"elephant\",\n  \"1f43c\": \"panda-face\",\n  \"1f40d\": \"snake\",\n  \"1f426\": \"bird\",\n  \"1f424\": \"baby-chick\",\n  \"1f425\": \"hatched-chick\",\n  \"1f423\": \"hatching-chick\",\n  \"1f414\": \"chicken\",\n  \"1f427\": \"penguin\",\n  \"1f422\": \"turtle\",\n  \"1f41b\": \"bug\",\n  \"1f41d\": \"honeybee\",\n  \"1f41c\": \"ant\",\n  \"1f41e\": \"beetle\",\n  \"1f40c\": \"snail\",\n  \"1f419\": \"octopus\",\n  \"1f420\": \"tropical-fish\",\n  \"1f41f\": \"fish\",\n  \"1f433\": \"whale\",\n  \"1f40b\": \"whale2\",\n  \"1f42c\": \"dolphin\",\n  \"1f404\": \"cow2\",\n  \"1f40f\": \"ram\",\n  \"1f400\": \"rat\",\n  \"1f403\": \"water-buffalo\",\n  \"1f405\": \"tiger2\",\n  \"1f407\": \"rabbit2\",\n  \"1f409\": \"dragon\",\n  \"1f410\": \"goat\",\n  \"1f413\": \"rooster\",\n  \"1f415\": \"dog2\",\n  \"1f416\": \"pig2\",\n  \"1f401\": \"mouse2\",\n  \"1f402\": \"ox\",\n  \"1f432\": \"dragon-face\",\n  \"1f421\": \"blowfish\",\n  \"1f40a\": \"crocodile\",\n  \"1f42a\": \"dromedary-camel\",\n  \"1f406\": \"leopard\",\n  \"1f408\": \"cat2\",\n  \"1f429\": \"poodle\",\n  \"1f43e\": \"paw-prints\",\n  \"1f490\": \"bouquet\",\n  \"1f338\": \"cherry-blossom\",\n  \"1f337\": \"tulip\",\n  \"1f340\": \"four-leaf-clover\",\n  \"1f339\": \"rose\",\n  \"1f33b\": \"sunflower\",\n  \"1f33a\": \"hibiscus\",\n  \"1f341\": \"maple-leaf\",\n  \"1f343\": \"leaves\",\n  \"1f342\": \"fallen-leaf\",\n  \"1f33f\": \"herb\",\n  \"1f344\": \"mushroom\",\n  \"1f335\": \"cactus\",\n  \"1f334\": \"palm-tree\",\n  \"1f332\": \"evergreen-tree\",\n  \"1f333\": \"deciduous-tree\",\n  \"1f330\": \"chestnut\",\n  \"1f331\": \"seedling\",\n  \"1f33c\": \"blossom\",\n  \"1f33e\": \"ear-of-rice\",\n  \"1f41a\": \"shell\",\n  \"1f310\": \"globe-with-meridians\",\n  \"1f31e\": \"sun-with-face\",\n  \"1f31d\": \"full-moon-with-face\",\n  \"1f31a\": \"new-moon-with-face\",\n  \"1f311\": \"new-moon\",\n  \"1f312\": \"waxing-crescent-moon\",\n  \"1f313\": \"first-quarter-moon\",\n  \"1f314\": \"waxing-gibbous-moon\",\n  \"1f315\": \"full-moon\",\n  \"1f316\": \"waning-gibbous-moon\",\n  \"1f317\": \"last-quarter-moon\",\n  \"1f318\": \"waning-crescent-moon\",\n  \"1f31c\": \"last-quarter-moon-with-face\",\n  \"1f31b\": \"first-quarter-moon-with-face\",\n  \"1f319\": \"moon\",\n  \"1f30d\": \"earth-africa\",\n  \"1f30e\": \"earth-americas\",\n  \"1f30f\": \"earth-asia\",\n  \"1f30b\": \"volcano\",\n  \"1f30c\": \"milky-way\",\n  \"26c5\": \"partly-sunny\",\n  \"1f38d\": \"bamboo\",\n  \"1f49d\": \"gift-heart\",\n  \"1f38e\": \"dolls\",\n  \"1f392\": \"school-satchel\",\n  \"1f393\": \"mortar-board\",\n  \"1f38f\": \"flags\",\n  \"1f386\": \"fireworks\",\n  \"1f387\": \"sparkler\",\n  \"1f390\": \"wind-chime\",\n  \"1f391\": \"rice-scene\",\n  \"1f383\": \"jack-o-lantern\",\n  \"1f47b\": \"ghost\",\n  \"1f385\": \"santa\",\n  \"1f3b1\": \"8ball\",\n  \"23f0\": \"alarm-clock\",\n  \"1f34e\": \"apple\",\n  \"1f3a8\": \"art\",\n  \"1f37c\": \"baby-bottle\",\n  \"1f388\": \"balloon\",\n  \"1f34c\": \"banana\",\n  \"1f4ca\": \"bar-chart\",\n  \"26be\": \"baseball\",\n  \"1f3c0\": \"basketball\",\n  \"1f6c0\": \"bath\",\n  \"1f6c1\": \"bathtub\",\n  \"1f50b\": \"battery\",\n  \"1f37a\": \"beer\",\n  \"1f37b\": \"beers\",\n  \"1f514\": \"bell\",\n  \"1f371\": \"bento\",\n  \"1f6b4\": \"bicyclist\",\n  \"1f459\": \"bikini\",\n  \"1f382\": \"birthday\",\n  \"1f0cf\": \"black-joker\",\n  \"2712\": \"black-nib\",\n  \"1f4d8\": \"blue-book\",\n  \"1f4a3\": \"bomb\",\n  \"1f516\": \"bookmark\",\n  \"1f4d1\": \"bookmark-tabs\",\n  \"1f4da\": \"books\",\n  \"1f462\": \"boot\",\n  \"1f3b3\": \"bowling\",\n  \"1f35e\": \"bread\",\n  \"1f4bc\": \"briefcase\",\n  \"1f4a1\": \"bulb\",\n  \"1f370\": \"cake\",\n  \"1f4c6\": \"calendar\",\n  \"1f4f2\": \"calling\",\n  \"1f4f7\": \"camera\",\n  \"1f36c\": \"candy\",\n  \"1f4c7\": \"card-index\",\n  \"1f4bf\": \"cd\",\n  \"1f4c9\": \"chart-with-downwards-trend\",\n  \"1f4c8\": \"chart-with-upwards-trend\",\n  \"1f352\": \"cherries\",\n  \"1f36b\": \"chocolate-bar\",\n  \"1f384\": \"christmas-tree\",\n  \"1f3ac\": \"clapper\",\n  \"1f4cb\": \"clipboard\",\n  \"1f4d5\": \"closed-book\",\n  \"1f510\": \"closed-lock-with-key\",\n  \"1f302\": \"closed-umbrella\",\n  \"2663\": \"clubs\",\n  \"1f378\": \"cocktail\",\n  \"2615\": \"coffee\",\n  \"1f4bb\": \"computer\",\n  \"1f38a\": \"confetti-ball\",\n  \"1f36a\": \"cookie\",\n  \"1f33d\": \"corn\",\n  \"1f4b3\": \"credit-card\",\n  \"1f451\": \"crown\",\n  \"1f52e\": \"crystal-ball\",\n  \"1f35b\": \"curry\",\n  \"1f36e\": \"custard\",\n  \"1f361\": \"dango\",\n  \"1f3af\": \"dart\",\n  \"1f4c5\": \"date\",\n  \"2666\": \"diamonds\",\n  \"1f4b5\": \"dollar\",\n  \"1f6aa\": \"door\",\n  \"1f369\": \"doughnut\",\n  \"1f457\": \"dress\",\n  \"1f4c0\": \"dvd\",\n  \"1f4e7\": \"e-mail\",\n  \"1f373\": \"egg\",\n  \"1f346\": \"eggplant\",\n  \"1f50c\": \"electric-plug\",\n  \"2709\": \"email\",\n  \"1f4b6\": \"euro\",\n  \"1f453\": \"eyeglasses\",\n  \"1f4e0\": \"fax\",\n  \"1f4c1\": \"file-folder\",\n  \"1f365\": \"fish-cake\",\n  \"1f3a3\": \"fishing-pole-and-fish\",\n  \"1f526\": \"flashlight\",\n  \"1f4be\": \"floppy-disk\",\n  \"1f3b4\": \"flower-playing-cards\",\n  \"1f3c8\": \"football\",\n  \"1f374\": \"fork-and-knife\",\n  \"1f364\": \"fried-shrimp\",\n  \"1f35f\": \"fries\",\n  \"1f3b2\": \"game-die\",\n  \"1f48e\": \"gem\",\n  \"1f381\": \"gift\",\n  \"26f3\": \"golf\",\n  \"1f347\": \"grapes\",\n  \"1f34f\": \"green-apple\",\n  \"1f4d7\": \"green-book\",\n  \"1f3b8\": \"guitar\",\n  \"1f52b\": \"gun\",\n  \"1f354\": \"hamburger\",\n  \"1f528\": \"hammer\",\n  \"1f45c\": \"handbag\",\n  \"1f3a7\": \"headphones\",\n  \"2665\": \"hearts\",\n  \"1f506\": \"high-brightness\",\n  \"1f460\": \"high-heel\",\n  \"1f52a\": \"hocho\",\n  \"1f36f\": \"honey-pot\",\n  \"1f3c7\": \"horse-racing\",\n  \"231b\": \"hourglass\",\n  \"23f3\": \"hourglass-flowing-sand\",\n  \"1f368\": \"ice-cream\",\n  \"1f366\": \"icecream\",\n  \"1f4e5\": \"inbox-tray\",\n  \"1f4e8\": \"incoming-envelope\",\n  \"1f4f1\": \"iphone\",\n  \"1f456\": \"jeans\",\n  \"1f511\": \"key\",\n  \"1f458\": \"kimono\",\n  \"1f4d2\": \"ledger\",\n  \"1f34b\": \"lemon\",\n  \"1f484\": \"lipstick\",\n  \"1f512\": \"lock\",\n  \"1f50f\": \"lock-with-ink-pen\",\n  \"1f36d\": \"lollipop\",\n  \"27bf\": \"loop\",\n  \"1f4e2\": \"loudspeaker\",\n  \"1f505\": \"low-brightness\",\n  \"1f50d\": \"mag\",\n  \"1f50e\": \"mag-right\",\n  \"1f004\": \"mahjong\",\n  \"1f4eb\": \"mailbox\",\n  \"1f4ea\": \"mailbox-closed\",\n  \"1f4ec\": \"mailbox-with-mail\",\n  \"1f4ed\": \"mailbox-with-no-mail\",\n  \"1f45e\": \"mans-shoe\",\n  \"1f356\": \"meat-on-bone\",\n  \"1f4e3\": \"mega\",\n  \"1f348\": \"melon\",\n  \"1f4dd\": \"memo\",\n  \"1f3a4\": \"microphone\",\n  \"1f52c\": \"microscope\",\n  \"1f4bd\": \"minidisc\",\n  \"1f4b8\": \"money-with-wings\",\n  \"1f4b0\": \"moneybag\",\n  \"1f6b5\": \"mountain-bicyclist\",\n  \"1f3a5\": \"movie-camera\",\n  \"1f3b9\": \"musical-keyboard\",\n  \"1f3bc\": \"musical-score\",\n  \"1f507\": \"mute\",\n  \"1f4db\": \"name-badge\",\n  \"1f454\": \"necktie\",\n  \"1f4f0\": \"newspaper\",\n  \"1f515\": \"no-bell\",\n  \"1f4d3\": \"notebook\",\n  \"1f4d4\": \"notebook-with-decorative-cover\",\n  \"1f529\": \"nut-and-bolt\",\n  \"1f362\": \"oden\",\n  \"1f4c2\": \"open-file-folder\",\n  \"1f4d9\": \"orange-book\",\n  \"1f4e4\": \"outbox-tray\",\n  \"1f4c4\": \"page-facing-up\",\n  \"1f4c3\": \"page-with-curl\",\n  \"1f4df\": \"pager\",\n  \"1f4ce\": \"paperclip\",\n  \"1f351\": \"peach\",\n  \"1f350\": \"pear\",\n  \"270f\": \"pencil2\",\n  \"260e\": \"phone\",\n  \"1f48a\": \"pill\",\n  \"1f34d\": \"pineapple\",\n  \"1f355\": \"pizza\",\n  \"1f4ef\": \"postal-horn\",\n  \"1f4ee\": \"postbox\",\n  \"1f45d\": \"pouch\",\n  \"1f357\": \"poultry-leg\",\n  \"1f4b7\": \"pound\",\n  \"1f45b\": \"purse\",\n  \"1f4cc\": \"pushpin\",\n  \"1f4fb\": \"radio\",\n  \"1f35c\": \"ramen\",\n  \"1f380\": \"ribbon\",\n  \"1f35a\": \"rice\",\n  \"1f359\": \"rice-ball\",\n  \"1f358\": \"rice-cracker\",\n  \"1f48d\": \"ring\",\n  \"1f3c9\": \"rugby-football\",\n  \"1f3bd\": \"running-shirt-with-sash\",\n  \"1f376\": \"sake\",\n  \"1f461\": \"sandal\",\n  \"1f4e1\": \"satellite\",\n  \"1f3b7\": \"saxophone\",\n  \"2702\": \"scissors\",\n  \"1f4dc\": \"scroll\",\n  \"1f4ba\": \"seat\",\n  \"1f367\": \"shaved-ice\",\n  \"1f455\": \"shirt\",\n  \"1f6bf\": \"shower\",\n  \"1f3bf\": \"ski\",\n  \"1f6ac\": \"smoking\",\n  \"1f3c2\": \"snowboarder\",\n  \"26bd\": \"soccer\",\n  \"1f509\": \"sound\",\n  \"1f47e\": \"space-invader\",\n  \"2660\": \"spades\",\n  \"1f35d\": \"spaghetti\",\n  \"1f50a\": \"speaker\",\n  \"1f372\": \"stew\",\n  \"1f4cf\": \"straight-ruler\",\n  \"1f353\": \"strawberry\",\n  \"1f3c4\": \"surfer\",\n  \"1f363\": \"sushi\",\n  \"1f360\": \"sweet-potato\",\n  \"1f3ca\": \"swimmer\",\n  \"1f489\": \"syringe\",\n  \"1f389\": \"tada\",\n  \"1f38b\": \"tanabata-tree\",\n  \"1f34a\": \"tangerine\",\n  \"1f375\": \"tea\",\n  \"1f4de\": \"telephone-receiver\",\n  \"1f52d\": \"telescope\",\n  \"1f3be\": \"tennis\",\n  \"1f6bd\": \"toilet\",\n  \"1f345\": \"tomato\",\n  \"1f3a9\": \"tophat\",\n  \"1f4d0\": \"triangular-ruler\",\n  \"1f3c6\": \"trophy\",\n  \"1f379\": \"tropical-drink\",\n  \"1f3ba\": \"trumpet\",\n  \"1f4fa\": \"tv\",\n  \"1f513\": \"unlock\",\n  \"1f4fc\": \"vhs\",\n  \"1f4f9\": \"video-camera\",\n  \"1f3ae\": \"video-game\",\n  \"1f3bb\": \"violin\",\n  \"231a\": \"watch\",\n  \"1f349\": \"watermelon\",\n  \"1f377\": \"wine-glass\",\n  \"1f45a\": \"womans-clothes\",\n  \"1f452\": \"womans-hat\",\n  \"1f527\": \"wrench\",\n  \"1f4b4\": \"yen\",\n  \"1f6a1\": \"aerial-tramway\",\n  \"2708\": \"airplane\",\n  \"1f691\": \"ambulance\",\n  \"2693\": \"anchor\",\n  \"1f69b\": \"articulated-lorry\",\n  \"1f3e7\": \"atm\",\n  \"1f3e6\": \"bank\",\n  \"1f488\": \"barber\",\n  \"1f530\": \"beginner\",\n  \"1f6b2\": \"bike\",\n  \"1f699\": \"blue-car\",\n  \"26f5\": \"boat\",\n  \"1f309\": \"bridge-at-night\",\n  \"1f685\": \"bullettrain-front\",\n  \"1f684\": \"bullettrain-side\",\n  \"1f68c\": \"bus\",\n  \"1f68f\": \"busstop\",\n  \"1f697\": \"car\",\n  \"1f3a0\": \"carousel-horse\",\n  \"1f3c1\": \"checkered-flag\",\n  \"26ea\": \"church\",\n  \"1f3aa\": \"circus-tent\",\n  \"1f307\": \"city-sunrise\",\n  \"1f306\": \"city-sunset\",\n  \"1f6a7\": \"construction\",\n  \"1f3ea\": \"convenience-store\",\n  \"1f38c\": \"crossed-flags\",\n  \"1f3ec\": \"department-store\",\n  \"1f3f0\": \"european-castle\",\n  \"1f3e4\": \"european-post-office\",\n  \"1f3ed\": \"factory\",\n  \"1f3a1\": \"ferris-wheel\",\n  \"1f692\": \"fire-engine\",\n  \"26f2\": \"fountain\",\n  \"26fd\": \"fuelpump\",\n  \"1f681\": \"helicopter\",\n  \"1f3e5\": \"hospital\",\n  \"1f3e8\": \"hotel\",\n  \"2668\": \"hotsprings\",\n  \"1f3e0\": \"house\",\n  \"1f3e1\": \"house-with-garden\",\n  \"1f5fe\": \"japan\",\n  \"1f3ef\": \"japanese-castle\",\n  \"1f688\": \"light-rail\",\n  \"1f3e9\": \"love-hotel\",\n  \"1f690\": \"minibus\",\n  \"1f69d\": \"monorail\",\n  \"1f5fb\": \"mount-fuji\",\n  \"1f6a0\": \"mountain-cableway\",\n  \"1f69e\": \"mountain-railway\",\n  \"1f5ff\": \"moyai\",\n  \"1f3e2\": \"office\",\n  \"1f698\": \"oncoming-automobile\",\n  \"1f68d\": \"oncoming-bus\",\n  \"1f694\": \"oncoming-police-car\",\n  \"1f696\": \"oncoming-taxi\",\n  \"1f3ad\": \"performing-arts\",\n  \"1f693\": \"police-car\",\n  \"1f3e3\": \"post-office\",\n  \"1f683\": \"railway-car\",\n  \"1f308\": \"rainbow\",\n  \"1f680\": \"rocket\",\n  \"1f3a2\": \"roller-coaster\",\n  \"1f6a8\": \"rotating-light\",\n  \"1f4cd\": \"round-pushpin\",\n  \"1f6a3\": \"rowboat\",\n  \"1f3eb\": \"school\",\n  \"1f6a2\": \"ship\",\n  \"1f3b0\": \"slot-machine\",\n  \"1f6a4\": \"speedboat\",\n  \"1f303\": \"stars\",\n  \"1f689\": \"station\",\n  \"1f5fd\": \"statue-of-liberty\",\n  \"1f682\": \"steam-locomotive\",\n  \"1f305\": \"sunrise\",\n  \"1f304\": \"sunrise-over-mountains\",\n  \"1f69f\": \"suspension-railway\",\n  \"1f695\": \"taxi\",\n  \"26fa\": \"tent\",\n  \"1f3ab\": \"ticket\",\n  \"1f5fc\": \"tokyo-tower\",\n  \"1f69c\": \"tractor\",\n  \"1f6a5\": \"traffic-light\",\n  \"1f686\": \"train2\",\n  \"1f68a\": \"tram\",\n  \"1f6a9\": \"triangular-flag-on-post\",\n  \"1f68e\": \"trolleybus\",\n  \"1f69a\": \"truck\",\n  \"1f6a6\": \"vertical-traffic-light\",\n  \"26a0\": \"warning\",\n  \"1f492\": \"wedding\",\n  \"1f1e6-1f1e9\": \"flag-ad\",\n  \"1f1e6-1f1ea\": \"flag-ae\",\n  \"1f1e6-1f1eb\": \"flag-af\",\n  \"1f1e6-1f1ec\": \"flag-ag\",\n  \"1f1e6-1f1ee\": \"flag-ai\",\n  \"1f1e6-1f1f1\": \"flag-al\",\n  \"1f1e6-1f1f2\": \"flag-am\",\n  \"1f1e6-1f1f4\": \"flag-ao\",\n  \"1f1e6-1f1f6\": \"flag-aq\",\n  \"1f1e6-1f1f7\": \"flag-ar\",\n  \"1f1e6-1f1f8\": \"flag-as\",\n  \"1f1e6-1f1f9\": \"flag-at\",\n  \"1f1e6-1f1fa\": \"flag-au\",\n  \"1f1e6-1f1fc\": \"flag-aw\",\n  \"1f1e6-1f1fd\": \"flag-ax\",\n  \"1f1e6-1f1ff\": \"flag-az\",\n  \"1f1e7-1f1e6\": \"flag-ba\",\n  \"1f1e7-1f1e7\": \"flag-bb\",\n  \"1f1e7-1f1e9\": \"flag-bd\",\n  \"1f1e7-1f1ea\": \"flag-be\",\n  \"1f1e7-1f1eb\": \"flag-bf\",\n  \"1f1e7-1f1ec\": \"flag-bg\",\n  \"1f1e7-1f1ed\": \"flag-bh\",\n  \"1f1e7-1f1ee\": \"flag-bi\",\n  \"1f1e7-1f1ef\": \"flag-bj\",\n  \"1f1e7-1f1f1\": \"flag-bl\",\n  \"1f1e7-1f1f2\": \"flag-bm\",\n  \"1f1e7-1f1f3\": \"flag-bn\",\n  \"1f1e7-1f1f4\": \"flag-bo\",\n  \"1f1e7-1f1f6\": \"flag-bq\",\n  \"1f1e7-1f1f7\": \"flag-br\",\n  \"1f1e7-1f1f8\": \"flag-bs\",\n  \"1f1e7-1f1f9\": \"flag-bt\",\n  \"1f1e7-1f1fb\": \"flag-bv\",\n  \"1f1e7-1f1fc\": \"flag-bw\",\n  \"1f1e7-1f1fe\": \"flag-by\",\n  \"1f1e7-1f1ff\": \"flag-bz\",\n  \"1f1e8-1f1e6\": \"flag-ca\",\n  \"1f1e8-1f1e8\": \"flag-cc\",\n  \"1f1e8-1f1e9\": \"flag-cd\",\n  \"1f1e8-1f1eb\": \"flag-cf\",\n  \"1f1e8-1f1ec\": \"flag-cg\",\n  \"1f1e8-1f1ed\": \"flag-ch\",\n  \"1f1e8-1f1ee\": \"flag-ci\",\n  \"1f1e8-1f1f0\": \"flag-ck\",\n  \"1f1e8-1f1f1\": \"flag-cl\",\n  \"1f1e8-1f1f2\": \"flag-cm\",\n  \"1f1e8-1f1f3\": \"flag-cn\",\n  \"1f1e8-1f1f4\": \"flag-co\",\n  \"1f1e8-1f1f7\": \"flag-cr\",\n  \"1f1e8-1f1fa\": \"flag-cu\",\n  \"1f1e8-1f1fb\": \"flag-cv\",\n  \"1f1e8-1f1fc\": \"flag-cw\",\n  \"1f1e8-1f1fd\": \"flag-cx\",\n  \"1f1e8-1f1fe\": \"flag-cy\",\n  \"1f1e8-1f1ff\": \"flag-cz\",\n  \"1f1e9-1f1ea\": \"flag-de\",\n  \"1f1e9-1f1ec\": \"flag-dg\",\n  \"1f1e9-1f1ef\": \"flag-dj\",\n  \"1f1e9-1f1f0\": \"flag-dk\",\n  \"1f1e9-1f1f2\": \"flag-dm\",\n  \"1f1e9-1f1f4\": \"flag-do\",\n  \"1f1e9-1f1ff\": \"flag-dz\",\n  \"1f1ea-1f1e8\": \"flag-ec\",\n  \"1f1ea-1f1ea\": \"flag-ee\",\n  \"1f1ea-1f1ec\": \"flag-eg\",\n  \"1f1ea-1f1ed\": \"flag-eh\",\n  \"1f1ea-1f1f7\": \"flag-er\",\n  \"1f1ea-1f1f8\": \"flag-es\",\n  \"1f1ea-1f1fa\": \"flag-eu\",\n  \"1f1ea-1f1f9\": \"flag-et\",\n  \"1f1eb-1f1ee\": \"flag-fi\",\n  \"1f1eb-1f1ef\": \"flag-fj\",\n  \"1f1eb-1f1f0\": \"flag-fk\",\n  \"1f1eb-1f1f2\": \"flag-fm\",\n  \"1f1eb-1f1f4\": \"flag-fo\",\n  \"1f1eb-1f1f7\": \"flag-fr\",\n  \"1f1ec-1f1e6\": \"flag-ga\",\n  \"1f1ec-1f1e7\": \"flag-gb\",\n  \"1f1ec-1f1e9\": \"flag-gd\",\n  \"1f1ec-1f1ea\": \"flag-ge\",\n  \"1f1ec-1f1eb\": \"flag-gf\",\n  \"1f1ec-1f1ec\": \"flag-gg\",\n  \"1f1ec-1f1ed\": \"flag-gh\",\n  \"1f1ec-1f1ee\": \"flag-gi\",\n  \"1f1ec-1f1f1\": \"flag-gl\",\n  \"1f1ec-1f1f2\": \"flag-gm\",\n  \"1f1ec-1f1f3\": \"flag-gn\",\n  \"1f1ec-1f1f5\": \"flag-gp\",\n  \"1f1ec-1f1f6\": \"flag-gq\",\n  \"1f1ec-1f1f7\": \"flag-gr\",\n  \"1f1ec-1f1f8\": \"flag-gs\",\n  \"1f1ec-1f1f9\": \"flag-gt\",\n  \"1f1ec-1f1fa\": \"flag-gu\",\n  \"1f1ec-1f1fc\": \"flag-gw\",\n  \"1f1ec-1f1fe\": \"flag-gy\",\n  \"1f1ed-1f1f0\": \"flag-hk\",\n  \"1f1ed-1f1f2\": \"flag-hm\",\n  \"1f1ed-1f1f3\": \"flag-hn\",\n  \"1f1ed-1f1f7\": \"flag-hr\",\n  \"1f1ed-1f1f9\": \"flag-ht\",\n  \"1f1ed-1f1fa\": \"flag-hu\",\n  \"1f1ee-1f1e9\": \"flag-id\",\n  \"1f1ee-1f1ea\": \"flag-ie\",\n  \"1f1ee-1f1f1\": \"flag-il\",\n  \"1f1ee-1f1f2\": \"flag-im\",\n  \"1f1ee-1f1f3\": \"flag-in\",\n  \"1f1ee-1f1f4\": \"flag-io\",\n  \"1f1ee-1f1f6\": \"flag-iq\",\n  \"1f1ee-1f1f7\": \"flag-ir\",\n  \"1f1ee-1f1f8\": \"flag-is\",\n  \"1f1ee-1f1f9\": \"flag-it\",\n  \"1f1ef-1f1ea\": \"flag-je\",\n  \"1f1ef-1f1f2\": \"flag-jm\",\n  \"1f1ef-1f1f4\": \"flag-jo\",\n  \"1f1ef-1f1f5\": \"flag-jp\",\n  \"1f1f0-1f1ea\": \"flag-ke\",\n  \"1f1f0-1f1ec\": \"flag-kg\",\n  \"1f1f0-1f1ed\": \"flag-kh\",\n  \"1f1f0-1f1ee\": \"flag-ki\",\n  \"1f1f0-1f1f2\": \"flag-km\",\n  \"1f1f0-1f1f3\": \"flag-kn\",\n  \"1f1f0-1f1f5\": \"flag-kp\",\n  \"1f1f0-1f1f7\": \"flag-kr\",\n  \"1f1f0-1f1fc\": \"flag-kw\",\n  \"1f1f0-1f1fe\": \"flag-ky\",\n  \"1f1f0-1f1ff\": \"flag-kz\",\n  \"1f1f1-1f1e6\": \"flag-la\",\n  \"1f1f1-1f1e7\": \"flag-lb\",\n  \"1f1f1-1f1e8\": \"flag-lc\",\n  \"1f1f1-1f1ee\": \"flag-li\",\n  \"1f1f1-1f1f0\": \"flag-lk\",\n  \"1f1f1-1f1f7\": \"flag-lr\",\n  \"1f1f1-1f1f8\": \"flag-ls\",\n  \"1f1f1-1f1f9\": \"flag-lt\",\n  \"1f1f1-1f1fa\": \"flag-lu\",\n  \"1f1f1-1f1fb\": \"flag-lv\",\n  \"1f1f1-1f1fe\": \"flag-ly\",\n  \"1f1f2-1f1e6\": \"flag-ma\",\n  \"1f1f2-1f1e8\": \"flag-mc\",\n  \"1f1f2-1f1e9\": \"flag-md\",\n  \"1f1f2-1f1ea\": \"flag-me\",\n  \"1f1f2-1f1eb\": \"flag-mf\",\n  \"1f1f2-1f1ec\": \"flag-mg\",\n  \"1f1f2-1f1ed\": \"flag-mh\",\n  \"1f1f2-1f1f0\": \"flag-mk\",\n  \"1f1f2-1f1f1\": \"flag-ml\",\n  \"1f1f2-1f1f2\": \"flag-mm\",\n  \"1f1f2-1f1f3\": \"flag-mn\",\n  \"1f1f2-1f1f4\": \"flag-mo\",\n  \"1f1f2-1f1f5\": \"flag-mp\",\n  \"1f1f2-1f1f6\": \"flag-mq\",\n  \"1f1f2-1f1f7\": \"flag-mr\",\n  \"1f1f2-1f1f8\": \"flag-ms\",\n  \"1f1f2-1f1f9\": \"flag-mt\",\n  \"1f1f2-1f1fa\": \"flag-mu\",\n  \"1f1f2-1f1fb\": \"flag-mv\",\n  \"1f1f2-1f1fc\": \"flag-mw\",\n  \"1f1f2-1f1fd\": \"flag-mx\",\n  \"1f1f2-1f1fe\": \"flag-my\",\n  \"1f1f2-1f1ff\": \"flag-mz\",\n  \"1f1f3-1f1e6\": \"flag-na\",\n  \"1f1f3-1f1e8\": \"flag-nc\",\n  \"1f1f3-1f1ea\": \"flag-ne\",\n  \"1f1f3-1f1eb\": \"flag-nf\",\n  \"1f1f3-1f1ec\": \"flag-ng\",\n  \"1f1f3-1f1ee\": \"flag-ni\",\n  \"1f1f3-1f1f1\": \"flag-nl\",\n  \"1f1f3-1f1f4\": \"flag-no\",\n  \"1f1f3-1f1f5\": \"flag-np\",\n  \"1f1f3-1f1f7\": \"flag-nr\",\n  \"1f1f3-1f1fa\": \"flag-nu\",\n  \"1f1f3-1f1ff\": \"flag-nz\",\n  \"1f1f4-1f1f2\": \"flag-om\",\n  \"1f1f5-1f1e6\": \"flag-pa\",\n  \"1f1f5-1f1ea\": \"flag-pe\",\n  \"1f1f5-1f1eb\": \"flag-pf\",\n  \"1f1f5-1f1ec\": \"flag-pg\",\n  \"1f1f5-1f1ed\": \"flag-ph\",\n  \"1f1f5-1f1f0\": \"flag-pk\",\n  \"1f1f5-1f1f1\": \"flag-pl\",\n  \"1f1f5-1f1f2\": \"flag-pm\",\n  \"1f1f5-1f1f3\": \"flag-pn\",\n  \"1f1f5-1f1f7\": \"flag-pr\",\n  \"1f1f5-1f1f8\": \"flag-ps\",\n  \"1f1f5-1f1f9\": \"flag-pt\",\n  \"1f1f5-1f1fc\": \"flag-pw\",\n  \"1f1f5-1f1fe\": \"flag-py\",\n  \"1f1f6-1f1e6\": \"flag-qa\",\n  \"1f1f7-1f1ea\": \"flag-re\",\n  \"1f1f7-1f1f4\": \"flag-ro\",\n  \"1f1f7-1f1f8\": \"flag-rs\",\n  \"1f1f7-1f1fa\": \"flag-ru\",\n  \"1f1f7-1f1fc\": \"flag-rw\",\n  \"1f1f8-1f1e6\": \"flag-sa\",\n  \"1f1f8-1f1e7\": \"flag-sb\",\n  \"1f1f8-1f1e8\": \"flag-sc\",\n  \"1f1f8-1f1e9\": \"flag-sd\",\n  \"1f1f8-1f1ea\": \"flag-se\",\n  \"1f1f8-1f1ec\": \"flag-sg\",\n  \"1f1f8-1f1ed\": \"flag-sh\",\n  \"1f1f8-1f1ee\": \"flag-si\",\n  \"1f1f8-1f1ef\": \"flag-sj\",\n  \"1f1f8-1f1f0\": \"flag-sk\",\n  \"1f1f8-1f1f1\": \"flag-sl\",\n  \"1f1f8-1f1f2\": \"flag-sm\",\n  \"1f1f8-1f1f3\": \"flag-sn\",\n  \"1f1f8-1f1f4\": \"flag-so\",\n  \"1f1f8-1f1f7\": \"flag-sr\",\n  \"1f1f8-1f1f8\": \"flag-ss\",\n  \"1f1f8-1f1f9\": \"flag-st\",\n  \"1f1f8-1f1fb\": \"flag-sv\",\n  \"1f1f8-1f1fd\": \"flag-sx\",\n  \"1f1f8-1f1fe\": \"flag-sy\",\n  \"1f1f8-1f1ff\": \"flag-sz\",\n  \"1f1f9-1f1e8\": \"flag-tc\",\n  \"1f1f9-1f1e9\": \"flag-td\",\n  \"1f1f9-1f1eb\": \"flag-tf\",\n  \"1f1f9-1f1ec\": \"flag-tg\",\n  \"1f1f9-1f1ed\": \"flag-th\",\n  \"1f1f9-1f1ef\": \"flag-tj\",\n  \"1f1f9-1f1f0\": \"flag-tk\",\n  \"1f1f9-1f1f1\": \"flag-tl\",\n  \"1f1f9-1f1f2\": \"flag-tm\",\n  \"1f1f9-1f1f3\": \"flag-tn\",\n  \"1f1f9-1f1f4\": \"flag-to\",\n  \"1f1f9-1f1f7\": \"flag-tr\",\n  \"1f1f9-1f1f9\": \"flag-tt\",\n  \"1f1f9-1f1fb\": \"flag-tv\",\n  \"1f1f9-1f1fc\": \"flag-tw\",\n  \"1f1f9-1f1ff\": \"flag-tz\",\n  \"1f1fa-1f1e6\": \"flag-ua\",\n  \"1f1fa-1f1ec\": \"flag-ug\",\n  \"1f1fa-1f1f2\": \"flag-um\",\n  \"1f1fa-1f1f8\": \"flag-us\",\n  \"1f1fa-1f1fe\": \"flag-uy\",\n  \"1f1fa-1f1ff\": \"flag-uz\",\n  \"1f1fb-1f1e6\": \"flag-va\",\n  \"1f1fb-1f1e8\": \"flag-vc\",\n  \"1f1fb-1f1ea\": \"flag-ve\",\n  \"1f1fb-1f1ec\": \"flag-vg\",\n  \"1f1fb-1f1ee\": \"flag-vi\",\n  \"1f1fb-1f1f3\": \"flag-vn\",\n  \"1f1fb-1f1fa\": \"flag-vu\",\n  \"1f1fc-1f1eb\": \"flag-wf\",\n  \"1f1fc-1f1f8\": \"flag-ws\",\n  \"1f1fd-1f1f0\": \"flag-xk\",\n  \"1f1fe-1f1ea\": \"flag-ye\",\n  \"1f1fe-1f1f9\": \"flag-yt\",\n  \"1f1ff-1f1e6\": \"flag-za\",\n  \"1f1ff-1f1f2\": \"flag-zm\",\n  \"1f1ff-1f1fc\": \"flag-zw\",\n  \"1f4af\": \"100\",\n  \"1f522\": \"1234\",\n  \"1f170\": \"a\",\n  \"1f18e\": \"ab\",\n  \"1f524\": \"abc\",\n  \"1f521\": \"abcd\",\n  \"1f251\": \"accept\",\n  \"2652\": \"aquarius\",\n  \"2648\": \"aries\",\n  \"25c0\": \"arrow-backward\",\n  \"23ec\": \"arrow-double-down\",\n  \"23eb\": \"arrow-double-up\",\n  \"2b07\": \"arrow-down\",\n  \"1f53d\": \"arrow-down-small\",\n  \"25b6\": \"arrow-forward\",\n  \"2935\": \"arrow-heading-down\",\n  \"2934\": \"arrow-heading-up\",\n  \"2b05\": \"arrow-left\",\n  \"2199\": \"arrow-lower-left\",\n  \"2198\": \"arrow-lower-right\",\n  \"27a1\": \"arrow-right\",\n  \"21aa\": \"arrow-right-hook\",\n  \"2b06\": \"arrow-up\",\n  \"2195\": \"arrow-up-down\",\n  \"1f53c\": \"arrow-up-small\",\n  \"2196\": \"arrow-upper-left\",\n  \"2197\": \"arrow-upper-right\",\n  \"1f503\": \"arrows-clockwise\",\n  \"1f504\": \"arrows-counterclockwise\",\n  \"1f171\": \"b\",\n  \"1f6bc\": \"baby-symbol\",\n  \"1f6c4\": \"baggage-claim\",\n  \"2611\": \"ballot-box-with-check\",\n  \"203c\": \"bangbang\",\n  \"26ab\": \"black-circle\",\n  \"1f532\": \"black-square-button\",\n  \"264b\": \"cancer\",\n  \"1f520\": \"capital-abcd\",\n  \"2651\": \"capricorn\",\n  \"1f4b9\": \"chart\",\n  \"1f6b8\": \"children-crossing\",\n  \"1f3a6\": \"cinema\",\n  \"1f191\": \"cl\",\n  \"1f550\": \"clock1\",\n  \"1f559\": \"clock10\",\n  \"1f565\": \"clock1030\",\n  \"1f55a\": \"clock11\",\n  \"1f566\": \"clock1130\",\n  \"1f55b\": \"clock12\",\n  \"1f567\": \"clock1230\",\n  \"1f55c\": \"clock130\",\n  \"1f551\": \"clock2\",\n  \"1f55d\": \"clock230\",\n  \"1f552\": \"clock3\",\n  \"1f55e\": \"clock330\",\n  \"1f553\": \"clock4\",\n  \"1f55f\": \"clock430\",\n  \"1f554\": \"clock5\",\n  \"1f560\": \"clock530\",\n  \"1f555\": \"clock6\",\n  \"1f561\": \"clock630\",\n  \"1f556\": \"clock7\",\n  \"1f562\": \"clock730\",\n  \"1f557\": \"clock8\",\n  \"1f563\": \"clock830\",\n  \"1f558\": \"clock9\",\n  \"1f564\": \"clock930\",\n  \"3297\": \"congratulations\",\n  \"1f192\": \"cool\",\n  \"a9\": \"copyright\",\n  \"27b0\": \"curly-loop\",\n  \"1f4b1\": \"currency-exchange\",\n  \"1f6c3\": \"customs\",\n  \"1f4a0\": \"diamond-shape-with-a-dot-inside\",\n  \"1f6af\": \"do-not-litter\",\n  \"38-20e3\": \"eight\",\n  \"2734\": \"eight-pointed-black-star\",\n  \"2733\": \"eight-spoked-asterisk\",\n  \"1f51a\": \"end\",\n  \"23e9\": \"fast-forward\",\n  \"35-20e3\": \"five\",\n  \"34-20e3\": \"four\",\n  \"1f193\": \"free\",\n  \"264a\": \"gemini\",\n  \"23-20e3\": \"hash\",\n  \"1f49f\": \"heart-decoration\",\n  \"2714\": \"heavy-check-mark\",\n  \"2797\": \"heavy-division-sign\",\n  \"1f4b2\": \"heavy-dollar-sign\",\n  \"2796\": \"heavy-minus-sign\",\n  \"2716\": \"heavy-multiplication-x\",\n  \"2795\": \"heavy-plus-sign\",\n  \"1f194\": \"id\",\n  \"1f250\": \"ideograph-advantage\",\n  \"2139\": \"information-source\",\n  \"2049\": \"interrobang\",\n  \"1f51f\": \"keycap-ten\",\n  \"1f201\": \"koko\",\n  \"1f535\": \"large-blue-circle\",\n  \"1f537\": \"large-blue-diamond\",\n  \"1f536\": \"large-orange-diamond\",\n  \"1f6c5\": \"left-luggage\",\n  \"2194\": \"left-right-arrow\",\n  \"21a9\": \"leftwards-arrow-with-hook\",\n  \"264c\": \"leo\",\n  \"264e\": \"libra\",\n  \"1f517\": \"link\",\n  \"24c2\": \"m\",\n  \"1f6b9\": \"mens\",\n  \"1f687\": \"metro\",\n  \"1f4f4\": \"mobile-phone-off\",\n  \"274e\": \"negative-squared-cross-mark\",\n  \"1f195\": \"new\",\n  \"1f196\": \"ng\",\n  \"39-20e3\": \"nine\",\n  \"1f6b3\": \"no-bicycles\",\n  \"26d4\": \"no-entry\",\n  \"1f6ab\": \"no-entry-sign\",\n  \"1f4f5\": \"no-mobile-phones\",\n  \"1f6b7\": \"no-pedestrians\",\n  \"1f6ad\": \"no-smoking\",\n  \"1f6b1\": \"non-potable-water\",\n  \"2b55\": \"o\",\n  \"1f17e\": \"o2\",\n  \"1f197\": \"ok\",\n  \"1f51b\": \"on\",\n  \"31-20e3\": \"one\",\n  \"26ce\": \"ophiuchus\",\n  \"1f17f\": \"parking\",\n  \"303d\": \"part-alternation-mark\",\n  \"1f6c2\": \"passport-control\",\n  \"2653\": \"pisces\",\n  \"1f6b0\": \"potable-water\",\n  \"1f6ae\": \"put-litter-in-its-place\",\n  \"1f518\": \"radio-button\",\n  \"267b\": \"recycle\",\n  \"1f534\": \"red-circle\",\n  \"ae\": \"registered\",\n  \"1f501\": \"repeat\",\n  \"1f502\": \"repeat-one\",\n  \"1f6bb\": \"restroom\",\n  \"23ea\": \"rewind\",\n  \"1f202\": \"sa\",\n  \"2650\": \"sagittarius\",\n  \"264f\": \"scorpius\",\n  \"3299\": \"secret\",\n  \"37-20e3\": \"seven\",\n  \"1f4f6\": \"signal-strength\",\n  \"36-20e3\": \"six\",\n  \"1f52f\": \"six-pointed-star\",\n  \"1f539\": \"small-blue-diamond\",\n  \"1f538\": \"small-orange-diamond\",\n  \"1f53a\": \"small-red-triangle\",\n  \"1f53b\": \"small-red-triangle-down\",\n  \"1f51c\": \"soon\",\n  \"1f198\": \"sos\",\n  \"1f523\": \"symbols\",\n  \"2649\": \"taurus\",\n  \"33-20e3\": \"three\",\n  \"2122\": \"tm\",\n  \"1f51d\": \"top\",\n  \"1f531\": \"trident\",\n  \"1f500\": \"twisted-rightwards-arrows\",\n  \"32-20e3\": \"two\",\n  \"1f239\": \"u5272\",\n  \"1f234\": \"u5408\",\n  \"1f23a\": \"u55b6\",\n  \"1f22f\": \"u6307\",\n  \"1f237\": \"u6708\",\n  \"1f236\": \"u6709\",\n  \"1f235\": \"u6e80\",\n  \"1f21a\": \"u7121\",\n  \"1f238\": \"u7533\",\n  \"1f232\": \"u7981\",\n  \"1f233\": \"u7a7a\",\n  \"1f51e\": \"underage\",\n  \"1f199\": \"up\",\n  \"1f4f3\": \"vibration-mode\",\n  \"264d\": \"virgo\",\n  \"1f19a\": \"vs\",\n  \"3030\": \"wavy-dash\",\n  \"1f6be\": \"wc\",\n  \"267f\": \"wheelchair\",\n  \"2705\": \"white-check-mark\",\n  \"26aa\": \"white-circle\",\n  \"1f4ae\": \"white-flower\",\n  \"1f533\": \"white-square-button\",\n  \"1f6ba\": \"womens\",\n  \"274c\": \"x\",\n  \"30-20e3\": \"zero\"\n);\n\n@each $code, $name in $emoji-map {\n  .twa-#{$name} {\n    background-image: url(\"https://twemoji.maxcdn.com/svg/#{$code}.svg\");\n  }\n}"
  },
  {
    "path": "client/modules/boot.js",
    "content": "export default {\n  readyStates: [],\n  callbacks: [],\n  /**\n   * Check if event has been sent\n   *\n   * @param {String} evt Event name\n   * @returns {Boolean} True if fired\n   */\n  isReady (evt) {\n    return this.readyStates.indexOf(evt) >= 0\n  },\n  /**\n   * Register a callback to be executed when event is sent\n   *\n   * @param {String} evt Event name to register to\n   * @param {Function} clb Callback function\n   * @param {Boolean} once If the callback should be called only once\n   */\n  register (evt, clb, once) {\n    if (this.isReady(evt)) {\n      clb()\n    } else {\n      this.callbacks.push({\n        event: evt,\n        callback: clb,\n        once: false,\n        called: false\n      })\n    }\n  },\n  /**\n   * Register a callback to be executed only once when event is sent\n   *\n   * @param {String} evt Event name to register to\n   * @param {Function} clb Callback function\n   */\n  registerOnce (evt, clb) {\n    this.register(evt, clb, true)\n  },\n  /**\n   * Set ready state and execute callbacks\n   */\n  notify (evt) {\n    this.readyStates.push(evt)\n    this.callbacks.forEach(clb => {\n      if (clb.event === evt) {\n        if (clb.once && clb.called) {\n          return\n        }\n        clb.called = true\n        clb.callback()\n      }\n    })\n  },\n  /**\n   * Execute callback on DOM ready\n   *\n   * @param {Function} clb Callback function\n   */\n  onDOMReady (clb) {\n    if (document.readyState === 'interactive' || document.readyState === 'complete' || document.readyState === 'loaded') {\n      clb()\n    } else {\n      document.addEventListener('DOMContentLoaded', clb)\n    }\n  }\n}\n"
  },
  {
    "path": "client/modules/localization.js",
    "content": "import i18next from 'i18next'\nimport Backend from 'i18next-chained-backend'\nimport LocalStorageBackend from 'i18next-localstorage-backend'\nimport i18nextXHR from 'i18next-xhr-backend'\nimport VueI18Next from '@panter/vue-i18next'\nimport _ from 'lodash'\n\n/* global siteConfig, graphQL */\n\nimport localeQuery from 'gql/common/common-localization-query-translations.gql'\n\nexport default {\n  VueI18Next,\n  init() {\n    i18next\n      .use(Backend)\n      .init({\n        backend: {\n          backends: [\n            LocalStorageBackend,\n            i18nextXHR\n          ],\n          backendOptions: [\n            {\n              expirationTime: 1000 * 60 * 60 * 24 // 24h\n            },\n            {\n              loadPath: '{{lng}}/{{ns}}',\n              parse: (data) => data,\n              ajax: (url, opts, cb, data) => {\n                let langParams = url.split('/')\n                graphQL.query({\n                  query: localeQuery,\n                  variables: {\n                    locale: langParams[0],\n                    namespace: langParams[1]\n                  }\n                }).then(resp => {\n                  let ns = {}\n                  if (_.get(resp, 'data.localization.translations', []).length > 0) {\n                    resp.data.localization.translations.forEach(entry => {\n                      _.set(ns, entry.key, entry.value)\n                    })\n                  }\n                  return cb(ns, {status: '200'})\n                }).catch(err => {\n                  console.error(err)\n                  return cb(null, {status: '404'})\n                })\n              }\n            }\n          ]\n        },\n        defaultNS: 'common',\n        lng: siteConfig.lang,\n        load: 'currentOnly',\n        lowerCaseLng: true,\n        fallbackLng: siteConfig.lang,\n        ns: ['common', 'auth']\n      })\n    return new VueI18Next(i18next)\n  }\n}\n"
  },
  {
    "path": "client/polyfills/array-from.js",
    "content": "// Production steps of ECMA-262, Edition 6, 22.1.2.1\nif (!Array.from) {\n  Array.from = (function () {\n    var toStr = Object.prototype.toString\n    var isCallable = function (fn) {\n      return typeof fn === 'function' || toStr.call(fn) === '[object Function]'\n    }\n    var toInteger = function (value) {\n      var number = Number(value)\n      if (isNaN(number)) { return 0 }\n      if (number === 0 || !isFinite(number)) { return number }\n      return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number))\n    }\n    var maxSafeInteger = Math.pow(2, 53) - 1\n    var toLength = function (value) {\n      var len = toInteger(value)\n      return Math.min(Math.max(len, 0), maxSafeInteger)\n    }\n\n    // The length property of the from method is 1.\n    return function from (arrayLike/*, mapFn, thisArg */) {\n      // 1. Let C be the this value.\n      var C = this\n\n      // 2. Let items be ToObject(arrayLike).\n      var items = Object(arrayLike)\n\n      // 3. ReturnIfAbrupt(items).\n      if (arrayLike == null) {\n        throw new TypeError('Array.from requires an array-like object - not null or undefined')\n      }\n\n      // 4. If mapfn is undefined, then let mapping be false.\n      var mapFn = arguments.length > 1 ? arguments[1] : void undefined\n      var T\n      if (typeof mapFn !== 'undefined') {\n        // 5. else\n        // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.\n        if (!isCallable(mapFn)) {\n          throw new TypeError('Array.from: when provided, the second argument must be a function')\n        }\n\n        // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.\n        if (arguments.length > 2) {\n          T = arguments[2]\n        }\n      }\n\n      // 10. Let lenValue be Get(items, \"length\").\n      // 11. Let len be ToLength(lenValue).\n      var len = toLength(items.length)\n\n      // 13. If IsConstructor(C) is true, then\n      // 13. a. Let A be the result of calling the [[Construct]] internal method\n      // of C with an argument list containing the single item len.\n      // 14. a. Else, Let A be ArrayCreate(len).\n      var A = isCallable(C) ? Object(new C(len)) : new Array(len)\n\n      // 16. Let k be 0.\n      var k = 0\n      // 17. Repeat, while k < len… (also steps a - h)\n      var kValue\n      while (k < len) {\n        kValue = items[k]\n        if (mapFn) {\n          A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k)\n        } else {\n          A[k] = kValue\n        }\n        k += 1\n      }\n      // 18. Let putStatus be Put(A, \"length\", len, true).\n      A.length = len\n      // 20. Return A.\n      return A\n    }\n  }())\n}\n"
  },
  {
    "path": "client/scss/app.scss",
    "content": "@import \"global\";\n\n@import \"base/base\";\n@import \"base/icons\";\n@import \"base/animation\";\n\n@import '~vuescroll/dist/vuescroll.css';\n@import '~katex/dist/katex.min.css';\n@import '~diff2html/bundles/css/diff2html.min.css';\n\n@import 'components/codemirror';\n@import 'components/katex';\n@import 'components/v-btn';\n@import 'components/v-data-table';\n@import 'components/v-dialog';\n@import 'components/v-form';\n@import 'components/v-tabs';\n\n// @import '../libs/twemoji/twemoji-awesome';\n// @import '../libs/prism/prism.css';\n// @import '~vue-tour/dist/vue-tour.css';\n// @import '~xterm/dist/xterm.css';\n// @import 'node_modules/diff2html/dist/diff2html.min';\n\n@import 'pages/new';\n@import 'pages/notfound';\n@import 'pages/unauthorized';\n@import 'pages/welcome';\n@import 'pages/error';\n\n@import 'layout/_rtl';\n"
  },
  {
    "path": "client/scss/base/animation.scss",
    "content": "$use-fade: true;\n$use-zoom: true;\n$use-bounce: true;\n\n@import \"~animate-sass/animate\";\n\n\n@for $i from 1 to 12 {\n  .wait-p#{$i}s {\n    animation-delay: $i * .1s !important;\n  }\n}\n"
  },
  {
    "path": "client/scss/base/base.scss",
    "content": "html {\n  box-sizing: border-box;\n  height: 100%;\n  overflow-y: auto !important;\n}\n*, *:before, *:after {\n  box-sizing: inherit;\n}\n\n[v-cloak], .is-hidden {\n  display: none;\n}\n\n#root {\n  position: relative;\n  min-height: 100%;\n\n  &.is-fullscreen {\n    height: 100vh;\n  }\n}\n\n.v-application--wrap {\n  transition: all 1.2s ease;\n  transform-origin: 50% 50%;\n  // background-color: #FFF;\n\n  @at-root .theme--dark & {\n    background-color: mc('grey', '900');\n  }\n}\n\n@media only screen and (min-width:960px) {\n  .v-application .v-footer {\n    padding-left: 272px\n  }\n}\n\n#root .v-application {\n  .overline {\n    line-height: 1rem;\n    font-size: .625rem!important;\n    font-weight: 400;\n    letter-spacing: .1666666667em!important;\n  }\n\n  @for $i from 0 through 25 {\n    .radius-#{$i} {\n      border-radius: #{$i}px;\n    }\n  }\n\n  @for $i from 1 through 5 {\n    .grey.darken-2-d#{$i} {\n      background-color: darken(mc('grey', '700'), percentage($i/100)) !important;\n      border-color: darken(mc('grey', '700'), percentage($i/100)) !important;\n    }\n    .grey.darken-2-l#{$i} {\n      background-color: lighten(mc('grey', '700'), percentage($i/100)) !important;\n      border-color: lighten(mc('grey', '700'), percentage($i/100)) !important;\n    }\n    .grey.darken-3-d#{$i} {\n      background-color: darken(mc('grey', '800'), percentage($i/100)) !important;\n      border-color: darken(mc('grey', '800'), percentage($i/100)) !important;\n    }\n    .grey.darken-3-l#{$i} {\n      background-color: lighten(mc('grey', '800'), percentage($i/100)) !important;\n      border-color: lighten(mc('grey', '800'), percentage($i/100)) !important;\n    }\n    .grey.darken-4-d#{$i} {\n      background-color: darken(mc('grey', '900'), percentage($i/100)) !important;\n      border-color: darken(mc('grey', '900'), percentage($i/100)) !important;\n    }\n    .grey.darken-4-l#{$i} {\n      background-color: lighten(mc('grey', '900'), percentage($i/100)) !important;\n      border-color: lighten(mc('grey', '900'), percentage($i/100)) !important;\n    }\n  }\n  .grey.darken-5 {\n    background-color: #0C0C0C !important;\n    border-color: #0C0C0C !important;\n  }\n\n  .blue.darken-5 {\n    background-color: darken(mc('blue', '900'), 20%) !important;\n    border-color: darken(mc('blue', '900'), 20%) !important;\n  }\n  .indigo.darken-5 {\n    background-color: darken(mc('indigo', '900'), 10%) !important;\n    border-color: darken(mc('indigo', '900'), 10%) !important;\n  }\n}\n"
  },
  {
    "path": "client/scss/base/icons.scss",
    "content": "// @font-face {\n//   font-family: 'Material Icons';\n//   font-style: normal;\n//   font-weight: 400;\n//   src: local('Material Icons'),\n//     local('MaterialIcons-Regular'),\n//     url(/fonts/MaterialIcons-Regular.woff2) format('woff2'),\n//     url(/fonts/MaterialIcons-Regular.woff) format('woff');\n// }\n\n// .material-icons {\n//   font-family: 'Material Icons', sans-serif;\n//   font-weight: normal;\n//   font-style: normal;\n//   font-size: 24px;  /* Preferred icon size */\n//   display: inline-flex;\n//   line-height: 1;\n//   text-transform: none;\n//   letter-spacing: normal;\n//   word-wrap: normal;\n//   white-space: nowrap;\n//   direction: ltr;\n\n//   /* Support for all WebKit browsers. */\n//   -webkit-font-smoothing: antialiased;\n//   /* Support for Safari and Chrome. */\n//   text-rendering: optimizeLegibility;\n\n//   /* Support for Firefox. */\n//   -moz-osx-font-smoothing: grayscale;\n\n//   /* Support for IE. */\n//   font-feature-settings: 'liga';\n// }\n\n.icons {\n  display: inline-block;\n  color: mc('grey', '800');\n  &.is-text {\n    display: inline-block;\n    width: 1em;\n    height: 1em;\n    vertical-align: middle;\n    position: relative;\n    top: -0.0625em;\n    stroke: none;\n    fill: none;\n  }\n  @each $size in 16,18,20,24,32,48,64,96,128 {\n    &.is-#{$size} {\n      width: #{$size}px;\n      height: #{$size}px;\n    }\n  }\n  &.has-right-pad {\n    margin-right: .5rem;\n  }\n  &.is-outlined {\n    stroke-width: 2px;\n    use {\n      fill: inherit;\n      stroke: mc('grey', '800');\n    }\n  }\n}\n.material-design-icon {\n  display: inline-flex;\n}\n"
  },
  {
    "path": "client/scss/base/material.scss",
    "content": "$material-colors: (\n  'red': (\n\t'50': #ffebee,\n\t'100': #ffcdd2,\n\t'200': #ef9a9a,\n\t'300': #e57373,\n\t'400': #ef5350,\n\t'500': #f44336,\n\t'600': #e53935,\n\t'700': #d32f2f,\n\t'800': #c62828,\n\t'900': #b71c1c,\n\t'a100': #ff8a80,\n\t'a200': #ff5252,\n\t'a400': #ff1744,\n\t'a700': #d50000\n  ),\n\n  'pink': (\n\t'50': #fce4ec,\n\t'100': #f8bbd0,\n\t'200': #f48fb1,\n\t'300': #f06292,\n\t'400': #ec407a,\n\t'500': #e91e63,\n\t'600': #d81b60,\n\t'700': #c2185b,\n\t'800': #ad1457,\n\t'900': #880e4f,\n\t'a100': #ff80ab,\n\t'a200': #ff4081,\n\t'a400': #f50057,\n\t'a700': #c51162\n  ),\n\n  'purple': (\n\t'50': #f3e5f5,\n\t'100': #e1bee7,\n\t'200': #ce93d8,\n\t'300': #ba68c8,\n\t'400': #ab47bc,\n\t'500': #9c27b0,\n\t'600': #8e24aa,\n\t'700': #7b1fa2,\n\t'800': #6a1b9a,\n\t'900': #4a148c,\n\t'a100': #ea80fc,\n\t'a200': #e040fb,\n\t'a400': #d500f9,\n\t'a700': #aa00ff\n  ),\n\n  'deep-purple': (\n\t'50': #ede7f6,\n\t'100': #d1c4e9,\n\t'200': #b39ddb,\n\t'300': #9575cd,\n\t'400': #7e57c2,\n\t'500': #673ab7,\n\t'600': #5e35b1,\n\t'700': #512da8,\n\t'800': #4527a0,\n\t'900': #311b92,\n\t'a100': #b388ff,\n\t'a200': #7c4dff,\n\t'a400': #651fff,\n\t'a700': #6200ea\n  ),\n\n  'indigo': (\n\t'50': #e8eaf6,\n\t'100': #c5cae9,\n\t'200': #9fa8da,\n\t'300': #7986cb,\n\t'400': #5c6bc0,\n\t'500': #3f51b5,\n\t'600': #3949ab,\n\t'700': #303f9f,\n\t'800': #283593,\n\t'900': #1a237e,\n\t'a100': #8c9eff,\n\t'a200': #536dfe,\n\t'a400': #3d5afe,\n\t'a700': #304ffe\n  ),\n\n  'blue': (\n\t'50': #e3f2fd,\n\t'100': #bbdefb,\n\t'200': #90caf9,\n\t'300': #64b5f6,\n\t'400': #42a5f5,\n\t'500': #2196f3,\n\t'600': #1e88e5,\n\t'700': #1976d2,\n\t'800': #1565c0,\n\t'900': #0d47a1,\n\t'a100': #82b1ff,\n\t'a200': #448aff,\n\t'a400': #2979ff,\n\t'a700': #2962ff\n  ),\n\n  'light-blue': (\n\t'50': #e1f5fe,\n\t'100': #b3e5fc,\n\t'200': #81d4fa,\n\t'300': #4fc3f7,\n\t'400': #29b6f6,\n\t'500': #03a9f4,\n\t'600': #039be5,\n\t'700': #0288d1,\n\t'800': #0277bd,\n\t'900': #01579b,\n\t'a100': #80d8ff,\n\t'a200': #40c4ff,\n\t'a400': #00b0ff,\n\t'a700': #0091ea\n  ),\n\n  'cyan': (\n\t'50': #e0f7fa,\n\t'100': #b2ebf2,\n\t'200': #80deea,\n\t'300': #4dd0e1,\n\t'400': #26c6da,\n\t'500': #00bcd4,\n\t'600': #00acc1,\n\t'700': #0097a7,\n\t'800': #00838f,\n\t'900': #006064,\n\t'a100': #84ffff,\n\t'a200': #18ffff,\n\t'a400': #00e5ff,\n\t'a700': #00b8d4\n  ),\n\n  'teal': (\n\t'50': #e0f2f1,\n\t'100': #b2dfdb,\n\t'200': #80cbc4,\n\t'300': #4db6ac,\n\t'400': #26a69a,\n\t'500': #009688,\n\t'600': #00897b,\n\t'700': #00796b,\n\t'800': #00695c,\n\t'900': #004d40,\n\t'a100': #a7ffeb,\n\t'a200': #64ffda,\n\t'a400': #1de9b6,\n\t'a700': #00bfa5\n  ),\n\n  'green': (\n\t'50': #e8f5e9,\n\t'100': #c8e6c9,\n\t'200': #a5d6a7,\n\t'300': #81c784,\n\t'400': #66bb6a,\n\t'500': #4caf50,\n\t'600': #43a047,\n\t'700': #388e3c,\n\t'800': #2e7d32,\n\t'900': #1b5e20,\n\t'a100': #b9f6ca,\n\t'a200': #69f0ae,\n\t'a400': #00e676,\n\t'a700': #00c853\n  ),\n\n  'light-green': (\n\t'50': #f1f8e9,\n\t'100': #dcedc8,\n\t'200': #c5e1a5,\n\t'300': #aed581,\n\t'400': #9ccc65,\n\t'500': #8bc34a,\n\t'600': #7cb342,\n\t'700': #689f38,\n\t'800': #558b2f,\n\t'900': #33691e,\n\t'a100': #ccff90,\n\t'a200': #b2ff59,\n\t'a400': #76ff03,\n\t'a700': #64dd17\n  ),\n\n  'lime': (\n\t'50': #f9fbe7,\n\t'100': #f0f4c3,\n\t'200': #e6ee9c,\n\t'300': #dce775,\n\t'400': #d4e157,\n\t'500': #cddc39,\n\t'600': #c0ca33,\n\t'700': #afb42b,\n\t'800': #9e9d24,\n\t'900': #827717,\n\t'a100': #f4ff81,\n\t'a200': #eeff41,\n\t'a400': #c6ff00,\n\t'a700': #aeea00\n  ),\n\n  'yellow': (\n\t'50': #fffde7,\n\t'100': #fff9c4,\n\t'200': #fff59d,\n\t'300': #fff176,\n\t'400': #ffee58,\n\t'500': #ffeb3b,\n\t'600': #fdd835,\n\t'700': #fbc02d,\n\t'800': #f9a825,\n\t'900': #f57f17,\n\t'a100': #ffff8d,\n\t'a200': #ffff00,\n\t'a400': #ffea00,\n\t'a700': #ffd600\n  ),\n\n  'amber': (\n\t'50': #fff8e1,\n\t'100': #ffecb3,\n\t'200': #ffe082,\n\t'300': #ffd54f,\n\t'400': #ffca28,\n\t'500': #ffc107,\n\t'600': #ffb300,\n\t'700': #ffa000,\n\t'800': #ff8f00,\n\t'900': #ff6f00,\n\t'a100': #ffe57f,\n\t'a200': #ffd740,\n\t'a400': #ffc400,\n\t'a700': #ffab00\n  ),\n\n  'orange': (\n\t'50': #fff3e0,\n\t'100': #ffe0b2,\n\t'200': #ffcc80,\n\t'300': #ffb74d,\n\t'400': #ffa726,\n\t'500': #ff9800,\n\t'600': #fb8c00,\n\t'700': #f57c00,\n\t'800': #ef6c00,\n\t'900': #e65100,\n\t'a100': #ffd180,\n\t'a200': #ffab40,\n\t'a400': #ff9100,\n\t'a700': #ff6d00\n  ),\n\n  'deep-orange': (\n\t'50': #fbe9e7,\n\t'100': #ffccbc,\n\t'200': #ffab91,\n\t'300': #ff8a65,\n\t'400': #ff7043,\n\t'500': #ff5722,\n\t'600': #f4511e,\n\t'700': #e64a19,\n\t'800': #d84315,\n\t'900': #bf360c,\n\t'a100': #ff9e80,\n\t'a200': #ff6e40,\n\t'a400': #ff3d00,\n\t'a700': #dd2c00\n  ),\n\n  'brown': (\n\t'50': #efebe9,\n\t'100': #d7ccc8,\n\t'200': #bcaaa4,\n\t'300': #a1887f,\n\t'400': #8d6e63,\n\t'500': #795548,\n\t'600': #6d4c41,\n\t'700': #5d4037,\n\t'800': #4e342e,\n\t'900': #3e2723\n  ),\n\n  'grey': (\n\t'50': #fafafa,\n\t'100': #f5f5f5,\n\t'200': #eeeeee,\n\t'300': #e0e0e0,\n\t'400': #bdbdbd,\n\t'500': #9e9e9e,\n\t'600': #757575,\n\t'700': #616161,\n\t'800': #424242,\n\t'900': #212121\n  ),\n\n  'blue-grey': (\n\t'50': #eceff1,\n\t'100': #cfd8dc,\n\t'200': #b0bec5,\n\t'300': #90a4ae,\n\t'400': #78909c,\n\t'500': #607d8b,\n\t'600': #546e7a,\n\t'700': #455a64,\n\t'800': #37474f,\n\t'900': #263238,\n\t'1000': #11171a\n  ),\n\n  'theme': (\n    'primary': #1976D2,\n    'secondary': #424242,\n    'accent': #82B1FF,\n    'error': #FF5252,\n    'info': #2196F3,\n    'success': #4CAF50,\n    'warning': #FFC107\n  )\n);\n\n@function mc($color-name, $color-variant: '500') {\n  $color: map-get(map-get($material-colors, $color-name),$color-variant);\n  @if $color {\n\t@return $color;\n  } @else {\n\t// Libsass still doesn't seem to support @error\n\t@warn \"=> ERROR: COLOR NOT FOUND! <= | Your $color-name, $color-variant combination did not match any of the values in the $material-colors map.\";\n  }\n}\n"
  },
  {
    "path": "client/scss/base/mixins.scss",
    "content": "/**\n* Placeholder attribute for inputs\n*\n* @return     {string}  Placeholder attributes\n*/\n@mixin placeholder {\n  &::-webkit-input-placeholder {@content;}\n  &::-moz-placeholder {@content;}\n  &:-ms-input-placeholder {@content;}\n  &:placeholder-shown {@content;}\n}\n\n/**\n* Spinner element\n*\n* @param      {string}  $color             - Color\n* @param      {string}  $dur               - Animation Duration\n* @param      {int}     $width             - Width\n* @param      {int}     $height  [$width]  - height\n*\n* @return     {string}  Spinner element\n*/\n@mixin spinner($color,$dur,$width,$height:$width) {\n  width: $width;\n  height: $height;\n  border-radius: 50%;\n  box-shadow: 0 0 0 1px rgba(0,0,0,0.1), 2px 1px 0 $color;\n  @include prefix(animation, spin $dur linear infinite);\n  @include keyframes(spin) {\n    100%{\n      @include prefix(transform, rotate(360deg));\n    }\n  }\n}\n\n/**\n* Prefixes for keyframes\n*\n* @param      {string}  $animation-name          - The animation name\n*\n* @return     {string}  Prefixed keyframes attributes\n*/\n@mixin keyframes($animation-name) {\n  @-webkit-keyframes #{$animation-name} {\n    @content;\n  }\n  @-moz-keyframes #{$animation-name} {\n    @content;\n  }\n  @-o-keyframes #{$animation-name} {\n    @content;\n  }\n  @keyframes #{$animation-name} {\n    @content;\n  }\n}\n\n/**\n* Prefix function for browser compatibility\n*\n* @param      {string}  $property          - Property name\n* @param      {any}     $value             - Property value\n*\n* @return     {string}  Prefixed attributes\n*/\n@mixin prefix($property, $value) {\n  -webkit-#{$property}: #{$value};\n  -moz-#{$property}: #{$value};\n  -ms-#{$property}: #{$value};\n  -o-#{$property}: #{$value};\n  #{$property}: #{$value};\n}\n\n/**\n* Layout Mixins\n*/\n@mixin from($device) {\n  @media screen and (min-width: $device) {\n    @content;\n  }\n}\n\n@mixin until($device) {\n  @media screen and (max-width: $device - 1px) {\n    @content;\n  }\n}\n\n@mixin mobile {\n  @media screen and (max-width: $tablet - 1px) {\n    @content;\n  }\n}\n\n@mixin tablet {\n  @media screen and (min-width: $tablet) {\n    @content;\n  }\n}\n\n@mixin tablet-only {\n  @media screen and (min-width: $tablet) and (max-width: $desktop - 1px) {\n    @content;\n  }\n}\n\n@mixin touch {\n  @media screen and (max-width: $desktop - 1px) {\n    @content;\n  }\n}\n\n@mixin desktop {\n  @media screen and (min-width: $desktop) {\n    @content;\n  }\n}\n\n@mixin desktop-only {\n  @media screen and (min-width: $desktop) and (max-width: $widescreen - 1px) {\n    @content;\n  }\n}\n\n@mixin widescreen {\n  @media screen and (min-width: $widescreen) {\n    @content;\n  }\n}\n\n// Nucleo Icons\n\n@mixin nc-rotate($degrees, $rotation) {\n  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});\n  -webkit-transform: rotate($degrees);\n  -moz-transform: rotate($degrees);\n  -ms-transform: rotate($degrees);\n  -o-transform: rotate($degrees);\n  transform: rotate($degrees);\n}\n\n@mixin nc-flip($horiz, $vert, $rotation) {\n  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});\n  -webkit-transform: scale($horiz, $vert);\n  -moz-transform: scale($horiz, $vert);\n  -ms-transform: scale($horiz, $vert);\n  -o-transform: scale($horiz, $vert);\n  transform: scale($horiz, $vert);\n}\n"
  },
  {
    "path": "client/scss/components/codemirror.scss",
    "content": ".cm-s-wikijs-dark.CodeMirror {\n  background: darken(mc('grey','900'), 3%);\n  color: #e0e0e0;\n}\n.cm-s-wikijs-dark div.CodeMirror-selected {\n  background: mc('blue','800');\n}\n.cm-s-wikijs-dark .cm-matchhighlight {\n  background: mc('blue','800');\n}\n.cm-s-wikijs-dark .CodeMirror-line::selection, .cm-s-wikijs-dark .CodeMirror-line > span::selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::selection {\n  background: mc('amber', '500');\n}\n.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 {\n  background: mc('amber', '500');\n}\n.cm-s-wikijs-dark .CodeMirror-gutters {\n  background: darken(mc('grey','900'), 6%);\n  border-right: 1px solid mc('grey','900');\n}\n.cm-s-wikijs-dark .CodeMirror-guttermarker {\n  color: #ac4142;\n}\n.cm-s-wikijs-dark .CodeMirror-guttermarker-subtle {\n  color: #505050;\n}\n.cm-s-wikijs-dark .CodeMirror-linenumber {\n  color: mc('grey','800');\n}\n.cm-s-wikijs-dark .CodeMirror-cursor {\n  border-left: 1px solid #b0b0b0;\n}\n.cm-s-wikijs-dark span.cm-comment {\n  color: mc('orange','800');\n}\n.cm-s-wikijs-dark span.cm-atom {\n  color: #aa759f;\n}\n.cm-s-wikijs-dark span.cm-number {\n  color: #aa759f;\n}\n.cm-s-wikijs-dark span.cm-property, .cm-s-wikijs-dark span.cm-attribute {\n  color: #90a959;\n}\n.cm-s-wikijs-dark span.cm-keyword {\n  color: #ac4142;\n}\n.cm-s-wikijs-dark span.cm-string {\n  color: #f4bf75;\n}\n.cm-s-wikijs-dark span.cm-variable {\n  color: #90a959;\n}\n.cm-s-wikijs-dark span.cm-variable-2 {\n  color: #6a9fb5;\n}\n.cm-s-wikijs-dark span.cm-def {\n  color: #d28445;\n}\n.cm-s-wikijs-dark span.cm-bracket {\n  color: #e0e0e0;\n}\n.cm-s-wikijs-dark span.cm-tag {\n  color: #ac4142;\n}\n.cm-s-wikijs-dark span.cm-link {\n  color: #aa759f;\n}\n.cm-s-wikijs-dark span.cm-error {\n  background: #ac4142;\n  color: #b0b0b0;\n}\n.cm-s-wikijs-dark .CodeMirror-activeline-background {\n  background: mc('grey','900');\n}\n.cm-s-wikijs-dark .CodeMirror-matchingbracket {\n  text-decoration: underline;\n  color: white !important;\n}\n\n.cm-s-wikijs-dark .CodeMirror-foldmarker {\n  margin-left: 10px;\n  display: inline-block;\n  background-color: rgba(mc('amber', '800'), .3);\n  padding: 8px 5px;\n  color: mc('amber', '500');\n  border-radius: 5px;\n  text-shadow: none;\n}\n\n.cm-s-wikijs-dark .CodeMirror-buttonmarker {\n  display: inline-block;\n  background-color: rgba(mc('blue', '500'), .3);\n  border: 1px solid mc('blue', '800');\n  padding: 1px 10px;\n  color: mc('blue', '200') !important;\n  border-radius: 5px;\n  margin-left: 5px;\n  cursor: pointer;\n}\n"
  },
  {
    "path": "client/scss/components/katex.scss",
    "content": ".v-application .katex .accent {\n  background-color: inherit !important;\n  border-color: inherit !important;\n}\n"
  },
  {
    "path": "client/scss/components/v-btn.scss",
    "content": ".v-btn.is-icon {\n  min-width: auto;\n}\n\n.btn-animate-rotate {\n  i {\n    transition: all 4s ease;\n    transform: rotate(0deg);\n  }\n  &:hover i {\n    transform: rotate(360deg);\n  }\n}\n\n.btn-animate-grow {\n  i {\n    transition: all 2s ease;\n    transform: scale(1);\n  }\n  &:hover i {\n    transform: scale(1.25);\n  }\n}\n\n.btn-animate-edit {\n  i {\n    transition: all .7s cubic-bezier(0.68, -0.55, 0.265, 1.55);\n    transform: rotate(0deg);\n  }\n  &:hover i {\n    transform: rotate(-45deg);\n  }\n}\n\n.btn-animate-wrench {\n  i {\n    transition: all .7s cubic-bezier(0.68, -0.55, 0.265, 1.55);\n    transform: rotate(0deg);\n  }\n  &:hover i {\n    transform: rotate(45deg);\n  }\n}\n\n.btn-animate-app {\n  i {\n    transition: all .6s ease;\n    transform: translate3d(0,0,0);\n    transform-style: preserve-3d;\n  }\n  &:hover i {\n    transform: scale(.7) rotateX(-180deg);\n  }\n}\n\n.btn-normalcase {\n  text-transform: none;\n}\n"
  },
  {
    "path": "client/scss/components/v-data-table.scss",
    "content": ".v-data-table {\n  .is-clickable {\n    cursor: pointer;\n  }\n}\n"
  },
  {
    "path": "client/scss/components/v-dialog.scss",
    "content": ".dialog-header {\n  background-color: mc('blue', '700');\n  background-image: radial-gradient(ellipse at top, mc('blue', '500'), mc('blue', '700')),\n                    radial-gradient(ellipse at bottom, mc('blue', '800'), mc('blue', '700'));\n  height: 60px;\n  color: #FFF;\n  display: flex;\n  align-items: center;\n  padding: 0 1rem;\n  font-size: 1.2rem;\n\n  &.is-red {\n    background-color: mc('red', '700');\n    background-image: radial-gradient(ellipse at top, mc('red', '500'), mc('red', '700')),\n                      radial-gradient(ellipse at bottom, mc('red', '800'), mc('red', '700'));\n  }\n\n  &.is-orange {\n    background-color: mc('orange', '700');\n    background-image: radial-gradient(ellipse at top, mc('orange', '600'), mc('orange', '800')),\n                      radial-gradient(ellipse at bottom, mc('orange', '900'), mc('orange', '800'));\n  }\n\n  &.is-indigo {\n    background-color: mc('indigo', '700');\n    background-image: radial-gradient(ellipse at top, mc('indigo', '500'), mc('indigo', '700')),\n                      radial-gradient(ellipse at bottom, mc('indigo', '800'), mc('indigo', '700'));\n  }\n\n  &.is-dark {\n    background-color: mc('grey', '900');\n    background-image: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')),\n                      radial-gradient(ellipse at bottom, mc('grey', '800'), mc('grey', '900'));\n  }\n\n  &.is-teal {\n    background-color: mc('teal', '700');\n    background-image: radial-gradient(ellipse at top, mc('teal', '500'), mc('teal', '700')),\n                      radial-gradient(ellipse at bottom, mc('teal', '800'), mc('teal', '700'));\n  }\n}\n\n.v-dialog--fullscreen {\n  @include until($tablet) {\n    padding-top: 56px;\n  }\n}\n"
  },
  {
    "path": "client/scss/components/v-form.scss",
    "content": ".wiki-form {\n\n  &.theme--light {\n    background-color: mc('grey', '50');\n  }\n\n  .v-text-field--outline {\n    .v-input__slot {\n      background-color: #FFF !important;\n      border-color: mc('grey', '300') !important;\n      border-radius: 7px;\n\n      @at-root .theme--dark & {\n        background-color: lighten(mc('grey', '900'), 5%) !important;\n        border-color: mc('grey', '700') !important;\n\n        .v-label.v-label--active.primary--text {\n          color: mc('blue', '500') !important;\n        }\n      }\n    }\n    &.v-input--is-focused .v-input__slot {\n      border-color: mc('blue', '500') !important;\n    }\n\n    @at-root .theme--dark & {\n      .v-icon.primary--text {\n        color: mc('blue', '500') !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/components/v-tabs.scss",
    "content": ".grad-tabs > .v-tabs-bar {\n  background-image: linear-gradient(to top, rgba(#000, .025), transparent);\n  border-bottom: 1px solid rgba(#000, .1);\n  border-radius: 4px 4px 0 0;\n\n  @at-root .theme--dark & {\n    background-image: linear-gradient(to bottom, rgba(#FFF, .05), transparent);\n    border-bottom-color: transparent;\n  }\n}\n"
  },
  {
    "path": "client/scss/fonts/arabic.scss",
    "content": "@font-face {\n  font-family: 'Tajawal';\n  src: url('../../fonts/arabic/Tajawal-Bold.woff2') format('woff2'),\n      url('../../fonts/arabic/Tajawal-Bold.woff') format('woff');\n  font-weight: bold;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Tajawal';\n  src: url('../../fonts/arabic/Tajawal-Regular.woff2') format('woff2'),\n      url('../../fonts/arabic/Tajawal-Regular.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Tajawal';\n  src: url('../../fonts/arabic/Tajawal-Medium.woff2') format('woff2'),\n      url('../../fonts/arabic/Tajawal-Medium.woff') format('woff');\n  font-weight: 500;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'BalooBhaijaan';\n  src: url('../../fonts/arabic/BalooBhaijaan-Regular.woff2') format('woff2'),\n      url('../../fonts/arabic/BalooBhaijaan-Regular.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Roboto Mono';\n  src: url('../../fonts/default/RobotoMono-Regular.woff2') format('woff2'),\n      url('../../fonts/default/RobotoMono-Regular.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n\nhtml:lang(ar) {\n  font-family: Tajawal, sans-serif;\n\n  .v-application {\n    font-family: Tajawal, sans-serif;\n\n    & .headline, & .title {\n      font-family: Tajawal, sans-serif !important;\n    }\n\n    &.v-application--is-rtl {\n      h1, h2, h3, h4, h5, h6 {\n        font-family: BalooBhaijaan, sans-serif;\n        font-weight: normal;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/fonts/default.scss",
    "content": "@font-face {\n  font-family: 'Roboto';\n  src: url('../../fonts/default/Roboto-MediumItalic.woff2') format('woff2'),\n      url('../../fonts/default/Roboto-MediumItalic.woff') format('woff');\n  font-weight: 500;\n  font-style: italic;\n}\n\n@font-face {\n  font-family: 'Roboto';\n  src: url('../../fonts/default/Roboto-Italic.woff2') format('woff2'),\n      url('../../fonts/default/Roboto-Italic.woff') format('woff');\n  font-weight: normal;\n  font-style: italic;\n}\n\n@font-face {\n  font-family: 'Roboto';\n  src: url('../../fonts/default/Roboto-Bold.woff2') format('woff2'),\n      url('../../fonts/default/Roboto-Bold.woff') format('woff');\n  font-weight: bold;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Roboto';\n  src: url('../../fonts/default/Roboto-Regular.woff2') format('woff2'),\n      url('../../fonts/default/Roboto-Regular.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Roboto';\n  src: url('../../fonts/default/Roboto-BoldItalic.woff2') format('woff2'),\n      url('../../fonts/default/Roboto-BoldItalic.woff') format('woff');\n  font-weight: bold;\n  font-style: italic;\n}\n\n@font-face {\n  font-family: 'Roboto';\n  src: url('../../fonts/default/Roboto-Medium.woff2') format('woff2'),\n      url('../../fonts/default/Roboto-Medium.woff') format('woff');\n  font-weight: 500;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Roboto Mono';\n  src: url('../../fonts/default/RobotoMono-Regular.woff2') format('woff2'),\n      url('../../fonts/default/RobotoMono-Regular.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n"
  },
  {
    "path": "client/scss/global.scss",
    "content": "@charset \"utf-8\";\n\n@import \"base/material\";\n@import \"base/mixins\";\n\n$tablet: 769px !default;\n$desktop: 980px !default;\n$widescreen: 1180px !default;\n\n$grid-breakpoints: (\n  'xs': 0,\n  'sm': 600px,\n  'md': 960px,\n  'lg': 1280px - 16px,\n  'xl': 1920px - 16px\n) !default;\n\n$display-breakpoints: (\n  'print-only': 'only print',\n  'screen-only': 'only screen',\n  'xs-only': 'only screen and (max-width: #{map-get($grid-breakpoints, 'sm') - 1})',\n  'sm-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'sm')}) and (max-width: #{map-get($grid-breakpoints, 'md') - 1})',\n  'sm-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'md') - 1})',\n  'sm-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'sm')})',\n  'md-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'md')}) and (max-width: #{map-get($grid-breakpoints, 'lg') - 1})',\n  'md-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'lg') - 1})',\n  'md-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'md')})',\n  'lg-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'lg')}) and (max-width: #{map-get($grid-breakpoints, 'xl') - 1})',\n  'lg-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'xl') - 1})',\n  'lg-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'lg')})',\n  'xl-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'xl')})'\n) !default;\n"
  },
  {
    "path": "client/scss/layout/_rtl.scss",
    "content": ".rtl {\n  direction: rtl;\n\n  .button i {\n    margin-left: 8px;\n    margin-right: 0px;\n  }\n\n  .nav-right .nav-item {\n    padding: 0 10px 0 0;\n  }\n\n  .nav-item h1 i {\n    margin-left: 8px;\n    margin-right: 8px;\n  }\n\n  .sidebar aside .sidebar-menu li a i {\n    margin-left: 7px;\n    margin-right: 0;\n  }\n\n  .mkcontent {\n    ul {\n      padding: 10px 40px 10px 0;\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/legacy.scss",
    "content": "@import \"global\";\n\n@import \"./base/icons.scss\";\n@import '~katex/dist/katex.min.css';\n@import '~@mdi/font/css/materialdesignicons.css';\n\n.mdi {\n  font-family: 'Material Design Icons', sans-serif;\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;  /* Preferred icon size */\n  display: inline-flex;\n  line-height: 1;\n  text-transform: none;\n  letter-spacing: normal;\n  word-wrap: normal;\n  white-space: nowrap;\n  direction: ltr;\n\n  /* Support for all WebKit browsers. */\n  -webkit-font-smoothing: antialiased;\n  /* Support for Safari and Chrome. */\n  text-rendering: optimizeLegibility;\n\n  /* Support for Firefox. */\n  -moz-osx-font-smoothing: grayscale;\n\n  /* Support for IE. */\n  font-feature-settings: 'liga';\n}\n\nhtml {\n  box-sizing: border-box;\n  background-color: mc('grey', '50');\n  font-size: 15px;\n}\n*, *:before, *:after {\n  box-sizing: inherit;\n}\n* {\n  margin: 0;\n  padding: 0;\n}\n\n.is-hidden {\n  display: none;\n}\n\nbody {\n  margin: 0;\n  padding: 0;\n  font-family: \"Roboto\",sans-serif;\n  line-height: 1.5;\n  min-height: 100vh;\n}\n\n// LOGIN\n\n.login {\n  background-color: mc('grey', '900');\n  height: 100vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  &-deprecated {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    background-color: mc('grey', '800');\n    text-align: center;\n    color: mc('grey', '50');\n    height: 64px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    a {\n      color: mc('red', '200');\n      margin-left: 5px;\n    }\n  }\n\n  &-error {\n    background-color: mc('red', '500');\n    color: #FFF;\n    padding: 5px;\n    border-radius: 5px;\n    margin-bottom: 2rem;\n  }\n\n  &-dialog {\n    width: 650px;\n    background-color: mc('grey', '100');\n    border-radius: 5px;\n    text-align: center;\n    padding: 2rem;\n    color: mc('grey', '800');\n\n    h1 {\n      margin-bottom: 2rem;\n    }\n\n    input, select {\n      display: block;\n      background-color: #FFF;\n      border: none;\n      border-radius: 5px;\n      width: 100%;\n      height: 40px;\n      padding: 0 1rem;\n      margin: 5px 0;\n    }\n\n    button {\n      height: 40px;\n      display: block;\n      width: 200px;\n      border: none;\n      border-radius: 5px;\n      margin: 0 auto;\n      background-color: mc('blue', '700');\n      color: #FFF;\n      cursor: pointer;\n      margin-top: 1rem;\n      font-weight: 600;\n\n      &:hover {\n        background-color: mc('blue', '800');\n      }\n    }\n  }\n\n  &-social {\n    margin-top: 2rem;\n    padding-top: 1rem;\n    border-top: 1px solid mc('grey', '400');\n\n    h2 {\n      font-size: 14px;\n      font-weight: 600;\n      margin-bottom: 1rem;\n    }\n\n    &-icon {\n      display: inline-flex;\n      justify-content: center;\n      align-items: center;\n      border-radius: 5px;\n      width: 54px;\n      height: 54px;\n      cursor: pointer;\n      transition: opacity .2s ease;\n      margin: .5rem .25rem;\n      &:hover {\n        opacity: .8;\n      }\n      svg {\n        width: 24px;\n        height: 24px;\n        bottom: 0;\n        path {\n          fill: #FFF;\n        }\n      }\n\n      @each $colorName, $color in $material-colors {\n        &.#{$colorName} {\n          background-color: map-get($color, '500');\n        }\n      }\n    }\n  }\n}\n\n// PAGE\n\n.header {\n  background-color: #000;\n  color: #FFF;\n  height: 64px;\n  padding: 0 16px;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n\n  &-title {\n    margin: 0;\n    font-size: 16px;\n    font-weight: 500;\n    letter-spacing: .02em;\n  }\n\n  &-deprecated {\n    color: mc('red', '100');\n\n    a {\n      color: mc('pink', '400');\n    }\n  }\n\n  &-login {\n    a {\n      text-decoration: none;\n      color: #FFF;\n      transition: color .3s ease;\n      border-radius: 50%;\n      background-color: mc('grey', '900');\n      display: flex;\n      width: 40px;\n      height: 40px;\n      justify-content: center;\n      align-items: center;\n\n      &:hover {\n        color: mc('blue', '500');\n      }\n    }\n  }\n}\n\n.main {\n  display: flex;\n  align-items: stretch;\n  min-height: calc(100vh - 64px);\n  height: 100%;\n\n  &-container {\n    flex-grow: 1;\n  }\n}\n\n.sidebar {\n  width: 256px;\n  background-color: mc('blue', '700');\n  color: #FFF;\n  padding: 8px 0;\n  align-self: stretch;\n  flex-shrink: 0;\n\n  .sidebar-link {\n    height: 40px;\n    font-size: 13px;\n    display: flex;\n    align-items: center;\n    padding: 0 16px;\n    transition: background .3s cubic-bezier(.25,.8,.5,1);\n    font-weight: 400;\n    color: #FFF;\n    text-decoration: none;\n\n    &:hover {\n      background: hsla(0,0%,100%,.08);\n    }\n  }\n\n  i.mdi {\n    width: 56px;\n    padding-left: 8px;\n  }\n\n  .sidebar-divider {\n    border-top: 1px solid hsla(0,0%,100%,.12);\n    margin: 8px 0;\n  }\n\n  .sidebar-title {\n    font-size: 13px;\n    height: 40px;\n    display: flex;\n    align-items: center;\n    padding: 0 16px 0 24px;\n    font-weight: 500;\n    color: hsla(0,0%,100%,.7);\n  }\n}\n\n.page-header {\n  background-color: mc('grey', '100');\n  padding: 0 24px;\n  height: 90px;\n  display: flex;\n  align-items: center;\n  border-bottom: 1px solid mc('grey', '200');\n\n  h1 {\n    font-size: 24px;\n    font-weight: 400;\n    line-height: 32px;\n    color: mc('grey', '800');\n  }\n\n  h2 {\n    color: mc('grey', '600');\n    font-size: 12px;\n    font-weight: 400;\n  }\n\n  &-left {\n    flex-grow: 1;\n  }\n\n  &-right {\n    flex: 0 0 308px;\n    padding-left: 16px;\n\n    &-title {\n      color: mc('grey', '500');\n      font-size: 12px;\n    }\n    &-author {\n      color: mc('grey', '800');\n      font-weight: 500;\n    }\n    &-updated {\n      color: mc('grey', '600');\n      font-size: 12px;\n    }\n  }\n}\n\n.page-contents {\n  display: flex;\n}\n\n.toc {\n  flex: 0 0 348px;\n  background-color: mc('grey', '200');\n  padding: 4px 0;\n\n  &-title {\n    font-size: 13px;\n    height: 40px;\n    display: flex;\n    color: mc('blue', '600');\n    align-items: center;\n    font-weight: 500;\n    padding: 0 16px;\n  }\n\n  &-tile {\n    text-decoration: none;\n    height: 40px;\n    display: flex;\n    font-size: 13px;\n    align-items: center;\n    padding: 0 16px;\n    color: mc('grey', '800');\n    transition: background-color .3s ease;\n\n    &.inset {\n      padding-left: 32px;\n    }\n\n    &:hover {\n      background-color: rgba(0,0,0,.06);\n    }\n  }\n\n  &-divider {\n    border-top: 1px solid rgba(0,0,0,.12);\n    margin: 0 0 0 24px;\n\n    &.inset {\n      margin-left: 40px;\n    }\n  }\n}\n\n@import \"../themes/default/scss/app.scss\";\n\n.contents {\n  flex-grow: 1;\n  padding: 24px !important;\n}\n"
  },
  {
    "path": "client/scss/pages/_error.scss",
    "content": ".app-error {\n  background: linear-gradient(to bottom, mc('grey', '900') 0%, mc('grey', '800') 100%);\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  color: mc('grey', '50');\n  font-family: Roboto, Arial, sans-serif;\n\n  img {\n    width: 250px;\n    filter: grayscale(50%) brightness(120%);\n    animation: errorlogo 5s linear infinite;\n    margin-bottom: 3rem;\n\n    @include until($tablet) {\n      width: 200px;\n    }\n  }\n\n  @keyframes errorlogo {\n    0% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg);\n    }\n    10% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(100%);\n    }\n    15% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(0%);\n    }\n    30% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg);\n    }\n    32% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(2700deg) invert(100%);\n    }\n    34% {\n      filter: blur(0) grayscale(100%) brightness(50%) hue-rotate(110deg);\n    }\n    50% {\n      filter: blur(0) grayscale(100%) brightness(200%) hue-rotate(110deg) sepia(0%);\n    }\n    55% {\n      filter: blur(0) grayscale(100%) brightness(100%) hue-rotate(110deg) sepia(100%);\n    }\n    60% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) sepia(0%);\n    }\n    90% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg);\n    }\n    95% {\n      filter: blur(5px) grayscale(50%) brightness(200%) hue-rotate(720deg);\n    }\n    100% {\n      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(100%);\n    }\n  }\n\n  > strong {\n    font-size: 1.5rem;\n  }\n\n  > span {\n    margin-top: 1rem;\n  }\n\n  > pre {\n    margin-top: 2rem;\n\n    code {\n      color: mc('grey', '500');\n      font-size: .8rem;\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/pages/_new.scss",
    "content": ".newpage {\n  background: linear-gradient(to bottom, darken(mc('blue', '900'), 10%) 0%, mc('purple', '500') 100%);\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  color: mc('grey', '50');\n\n  &::before {\n    content: '';\n    display:block;\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n    background-image: url('../static/svg/motif-circuit.svg');\n    background-position: center center;\n    background-repeat: repeat;\n    background-size: 200px;\n    z-index: 0;\n    opacity: .75;\n    animation: onboardingBgReveal 80s linear infinite;\n\n    @include keyframes(onboardingBgReveal) {\n      0% {\n        background-position-y: 0;\n      }\n      100% {\n        background-position-y: -2000px;\n      }\n    }\n  }\n\n  &::after {\n    content: '';\n    position: absolute;\n    background-color: transparent;\n    background-image: url('../static/svg/motif-overlay.svg');\n    background-attachment: fixed;\n    background-size: cover;\n    opacity: .5;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n  }\n\n  &-content {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    z-index: 2;\n  }\n\n  img {\n    height: 250px;\n    margin-bottom: 3rem;\n    z-index: 2;\n    animation-duration: 2s;\n\n    @include until($tablet) {\n      height: 200px;\n    }\n  }\n\n  h1 {\n    font-size: 1.5rem;\n    margin-bottom: 1rem;\n    z-index: 2;\n  }\n  h2 {\n    margin-bottom: 3rem;\n    z-index: 2;\n  }\n  .v-btn {\n    z-index: 2;\n  }\n}\n"
  },
  {
    "path": "client/scss/pages/_notfound.scss",
    "content": ".notfound {\n  background: linear-gradient(to bottom, darken(mc('red', '900'), 25%) 0%, mc('red', '600') 100%);\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  color: mc('grey', '50');\n\n  &::before {\n    content: '';\n    display:block;\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n    background-image: url('../static/svg/motif-circuit.svg');\n    background-position: center center;\n    background-repeat: repeat;\n    background-size: 200px;\n    z-index: 0;\n    opacity: .75;\n    animation: onboardingBgReveal 80s linear infinite;\n\n    @include keyframes(onboardingBgReveal) {\n      0% {\n        background-position-y: 0;\n      }\n      100% {\n        background-position-y: -2000px;\n      }\n    }\n  }\n\n  &::after {\n    content: '';\n    position: absolute;\n    background-color: transparent;\n    background-image: url('../static/svg/motif-overlay.svg');\n    background-attachment: fixed;\n    background-size: cover;\n    opacity: .5;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n  }\n\n  &-content {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    z-index: 2;\n  }\n\n  img {\n    height: 250px;\n    margin-bottom: 3rem;\n    z-index: 2;\n    animation-duration: 2s;\n\n    @include until($tablet) {\n      height: 200px;\n    }\n  }\n\n  h1 {\n    font-size: 1.5rem;\n    margin-bottom: 1rem;\n    z-index: 2;\n  }\n  h2 {\n    margin-bottom: 3rem;\n    z-index: 2;\n  }\n  .v-btn {\n    z-index: 2;\n  }\n}\n"
  },
  {
    "path": "client/scss/pages/_unauthorized.scss",
    "content": ".unauthorized {\n  background: linear-gradient(to bottom, darken(mc('blue', '900'), 10%) 0%, mc('red', '500') 100%);\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  color: mc('grey', '50');\n\n  &::before {\n    content: '';\n    display:block;\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n    background-image: url('../static/svg/motif-diagonals.svg');\n    background-position: center center;\n    background-repeat: repeat;\n    background-size: 50px;\n    z-index: 0;\n    opacity: .75;\n    animation: onboardingBgReveal 50s linear infinite;\n\n    @include keyframes(onboardingBgReveal) {\n      0% {\n        background-position-y: 0;\n      }\n      100% {\n        background-position-y: -2000px;\n      }\n    }\n  }\n\n  &::after {\n    content: '';\n    position: absolute;\n    background-color: transparent;\n    background-image: url('../static/svg/motif-overlay.svg');\n    background-attachment: fixed;\n    background-size: cover;\n    opacity: .5;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n  }\n\n  &-content {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    z-index: 2;\n  }\n\n  img {\n    height: 250px;\n    margin-bottom: 3rem;\n    z-index: 2;\n    animation-duration: 2s;\n\n    @include until($tablet) {\n      height: 200px;\n    }\n  }\n\n  h1 {\n    font-size: 1.5rem;\n    margin-bottom: 1rem;\n    z-index: 2;\n  }\n  h2 {\n    margin-bottom: 3rem;\n    z-index: 2;\n  }\n  .v-btn {\n    z-index: 2;\n  }\n}\n"
  },
  {
    "path": "client/scss/pages/_welcome.scss",
    "content": ".onboarding {\n  background: linear-gradient(to bottom, mc('grey', '900') 0%, mc('grey', '700') 100%);\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  color: mc('grey', '50');\n\n  &::before {\n    content: '';\n    display:block;\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n    background-image: url('../static/svg/motif-blocks.svg');\n    background-position: center center;\n    background-repeat: repeat;\n    background-size: 500px;\n    z-index: 0;\n    opacity: .75;\n    animation: onboardingBgReveal 50s linear infinite;\n\n    @include keyframes(onboardingBgReveal) {\n      0% {\n        background-position-y: 0;\n      }\n      100% {\n        background-position-y: -2000px;\n      }\n    }\n  }\n\n  &-content {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    z-index: 2;\n  }\n\n  img {\n    width: 500px;\n    filter: grayscale(100%) brightness(160%);\n    margin-bottom: 3rem;\n    z-index: 2;\n    animation-duration: 3s;\n\n    @include until($tablet) {\n      width: 300px;\n    }\n  }\n  h1 {\n    font-size: 1.5rem;\n    margin-bottom: 1rem;\n    z-index: 2;\n  }\n  h2 {\n    margin-bottom: 3rem;\n    z-index: 2;\n  }\n  .v-btn {\n    z-index: 2;\n  }\n}\n"
  },
  {
    "path": "client/static/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n  <msapplication>\n    <tile>\n      <square150x150logo src=\"/_assets/favicons/ms-icon-150x150.png\"/>\n      <TileColor>#1976d2</TileColor>\n    </tile>\n  </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "client/static/favicons/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/mstile-150x150.png\"/>\n            <TileColor>#1976d2</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "client/static/manifest.json",
    "content": "{\n  \"name\": \"Wiki.js\",\n  \"short_name\": \"Wiki.js\",\n  \"start_url\": \"/\",\n  \"icons\": [\n      {\n          \"src\": \"/_assets/favicons/android-chrome-192x192.png\",\n          \"sizes\": \"192x192\",\n          \"type\": \"image/png\"\n      },\n      {\n          \"src\": \"/_assets/favicons/android-chrome-256x256.png\",\n          \"sizes\": \"256x256\",\n          \"type\": \"image/png\"\n      }\n  ],\n  \"theme_color\": \"#1976d2\",\n  \"background_color\": \"#1976d2\",\n  \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "client/store/admin.js",
    "content": "import { make } from 'vuex-pathify'\n\nconst state = {\n  info: {\n    currentVersion: 'n/a',\n    latestVersion: 'n/a',\n    groupsTotal: 0,\n    pagesTotal: 0,\n    usersTotal: 0\n  }\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations: make.mutations(state)\n}\n"
  },
  {
    "path": "client/store/editor.js",
    "content": "import { make } from 'vuex-pathify'\n\nconst state = {\n  editor: '',\n  editorKey: '',\n  content: '',\n  mode: 'create',\n  activeModal: '',\n  activeModalData: null,\n  media: {\n    folderTree: [],\n    currentFolderId: 0,\n    currentFileId: null\n  },\n  checkoutDateActive: ''\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations: {\n    ...make.mutations(state),\n    pushMediaFolderTree: (st, folder) => {\n      st.media.folderTree = st.media.folderTree.concat(folder)\n    },\n    popMediaFolderTree: (st) => {\n      st.media.folderTree = st.media.folderTree.slice(0, -1)\n    }\n  }\n}\n"
  },
  {
    "path": "client/store/index.js",
    "content": "import _ from 'lodash'\nimport Vue from 'vue'\nimport Vuex from 'vuex'\nimport pathify from 'vuex-pathify' // eslint-disable-line import/no-duplicates\nimport { make } from 'vuex-pathify' // eslint-disable-line import/no-duplicates\n\nimport page from './page'\nimport site from './site'\nimport user from './user'\n\n/* global WIKI */\n\nVue.use(Vuex)\n\nconst state = {\n  loadingStack: [],\n  notification: {\n    message: '',\n    style: 'primary',\n    icon: 'cached',\n    isActive: false\n  }\n}\n\nexport default new Vuex.Store({\n  strict: process.env.NODE_ENV !== 'production',\n  plugins: [\n    pathify.plugin\n  ],\n  state,\n  getters: {\n    isLoading: state => { return state.loadingStack.length > 0 }\n  },\n  mutations: {\n    ...make.mutations(state),\n    loadingStart (st, stackName) {\n      st.loadingStack = _.union(st.loadingStack, [stackName])\n    },\n    loadingStop (st, stackName) {\n      st.loadingStack = _.without(st.loadingStack, stackName)\n    },\n    showNotification (st, opts) {\n      st.notification = _.defaults(opts, {\n        message: '',\n        style: 'primary',\n        icon: 'cached',\n        isActive: true\n      })\n    },\n    updateNotificationState (st, newState) {\n      st.notification.isActive = newState\n    },\n    pushGraphError (st, err) {\n      WIKI.$store.commit('showNotification', {\n        style: 'red',\n        message: _.get(err, 'graphQLErrors[0].message', err.message),\n        icon: 'alert'\n      })\n    }\n  },\n  actions: { },\n  modules: {\n    page,\n    site,\n    user\n  }\n})\n"
  },
  {
    "path": "client/store/page.js",
    "content": "import { make } from 'vuex-pathify'\n\nconst state = {\n  id: 0,\n  authorId: 0,\n  authorName: 'Unknown',\n  createdAt: '',\n  description: '',\n  isPublished: true,\n  locale: 'en',\n  path: '',\n  publishEndDate: '',\n  publishStartDate: '',\n  tags: [],\n  title: '',\n  updatedAt: '',\n  editor: '',\n  mode: '',\n  scriptJs: '',\n  scriptCss: '',\n  effectivePermissions: {\n    comments: {\n      read: false,\n      write: false,\n      manage: false\n    },\n    history: {\n      read: false\n    },\n    source: {\n      read: false\n    },\n    pages: {\n      write: false,\n      manage: false,\n      delete: false,\n      script: false,\n      style: false\n    },\n    system: {\n      manage: false\n    }\n  },\n  commentsCount: 0,\n  editShortcuts: {\n    editFab: false,\n    editMenuBar: false,\n    editMenuBtn: false,\n    editMenuExternalBtn: false,\n    editMenuExternalName: '',\n    editMenuExternalIcon: '',\n    editMenuExternalUrl: ''\n  }\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations: make.mutations(state)\n}\n"
  },
  {
    "path": "client/store/site.js",
    "content": "import { make } from 'vuex-pathify'\n\n/* global siteConfig */\n\nconst state = {\n  company: siteConfig.company,\n  contentLicense: siteConfig.contentLicense,\n  footerOverride: siteConfig.footerOverride,\n  dark: siteConfig.darkMode,\n  tocPosition: siteConfig.tocPosition,\n  mascot: true,\n  title: siteConfig.title,\n  logoUrl: siteConfig.logoUrl,\n  search: '',\n  searchIsFocused: false,\n  searchIsLoading: false,\n  searchRestrictLocale: false,\n  searchRestrictPath: false,\n  printView: false\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations: make.mutations(state)\n}\n"
  },
  {
    "path": "client/store/user.js",
    "content": "import { make } from 'vuex-pathify'\nimport jwt from 'jsonwebtoken'\nimport Cookies from 'js-cookie'\n\nconst state = {\n  id: 0,\n  email: '',\n  name: '',\n  pictureUrl: '',\n  localeCode: '',\n  defaultEditor: '',\n  timezone: '',\n  dateFormat: '',\n  appearance: '',\n  permissions: [],\n  iat: 0,\n  exp: 0,\n  authenticated: false\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations: {\n    ...make.mutations(state),\n    REFRESH_AUTH(st) {\n      const jwtCookie = Cookies.get('jwt')\n      if (jwtCookie) {\n        try {\n          const jwtData = jwt.decode(jwtCookie)\n          st.id = jwtData.id\n          st.email = jwtData.email\n          st.name = jwtData.name\n          st.pictureUrl = jwtData.av\n          st.localeCode = jwtData.lc\n          st.timezone = jwtData.tz || Intl.DateTimeFormat().resolvedOptions().timeZone || ''\n          st.dateFormat = jwtData.df || ''\n          st.appearance = jwtData.ap || ''\n          // st.defaultEditor = jwtData.defaultEditor\n          st.permissions = jwtData.permissions\n          st.iat = jwtData.iat\n          st.exp = jwtData.exp\n          st.authenticated = true\n        } catch (err) {\n          console.debug('Invalid JWT. Silent authentication skipped.')\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/themes/default/components/nav-footer.vue",
    "content": "<template lang=\"pug\">\n  v-footer.justify-center(:color='bgColor', inset)\n    .caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`')\n      template(v-if='footerOverride')\n        span(v-html='footerOverrideRender + ` |&nbsp;`')\n      template(v-else-if='company && company.length > 0 && contentLicense !== ``')\n        span(v-if='contentLicense === `alr`') {{ $t('common:footer.copyright', { company: company, year: currentYear, interpolation: { escapeValue: false } }) }} |&nbsp;\n        span(v-else) {{ $t('common:footer.license', { company: company, license: $t('common:license.' + contentLicense), interpolation: { escapeValue: false } }) }} |&nbsp;\n      span {{ $t('common:footer.poweredBy') }} #[a(href='https://wiki.js.org', ref='nofollow') Wiki.js]\n</template>\n\n<script>\nimport { get } from 'vuex-pathify'\nimport MarkdownIt from 'markdown-it'\n\nconst md = new MarkdownIt({\n  html: false,\n  breaks: false,\n  linkify: true\n})\n\nexport default {\n  props: {\n    color: {\n      type: String,\n      default: 'grey lighten-3'\n    },\n    darkColor: {\n      type: String,\n      default: 'grey darken-3'\n    }\n  },\n  data() {\n    return {\n      currentYear: (new Date()).getFullYear()\n    }\n  },\n  computed: {\n    company: get('site/company'),\n    contentLicense: get('site/contentLicense'),\n    footerOverride: get('site/footerOverride'),\n    footerOverrideRender () {\n      if (!this.footerOverride) { return '' }\n      return md.renderInline(this.footerOverride)\n    },\n    bgColor() {\n      if (!this.$vuetify.theme.dark) {\n        return this.color\n      } else {\n        return this.darkColor\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n  .v-footer {\n    a {\n      text-decoration: none;\n    }\n\n    &.altbg {\n      background: mc('theme', 'primary');\n\n      span {\n        color: mc('blue', '300');\n      }\n\n      a {\n        color: mc('blue', '200');\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "client/themes/default/components/nav-sidebar.vue",
    "content": "<template lang=\"pug\">\n  div\n    .pa-3.d-flex(v-if='navMode === `MIXED`', :class='$vuetify.theme.dark ? `grey darken-5` : `blue darken-3`')\n      v-btn(\n        depressed\n        :color='$vuetify.theme.dark ? `grey darken-4` : `blue darken-2`'\n        style='min-width:0;'\n        @click='goHome'\n        :aria-label='$t(`common:header.home`)'\n        )\n        v-icon(size='20') mdi-home\n      v-btn.ml-3(\n        v-if='currentMode === `custom`'\n        depressed\n        :color='$vuetify.theme.dark ? `grey darken-4` : `blue darken-2`'\n        style='flex: 1 1 100%;'\n        @click='switchMode(`browse`)'\n        )\n        v-icon(left) mdi-file-tree\n        .body-2.text-none {{$t('common:sidebar.browse')}}\n      v-btn.ml-3(\n        v-else-if='currentMode === `browse`'\n        depressed\n        :color='$vuetify.theme.dark ? `grey darken-4` : `blue darken-2`'\n        style='flex: 1 1 100%;'\n        @click='switchMode(`custom`)'\n        )\n        v-icon(left) mdi-navigation\n        .body-2.text-none {{$t('common:sidebar.mainMenu')}}\n    v-divider\n    //-> Custom Navigation\n    v-list.py-2(v-if='currentMode === `custom`', dense, :class='color', :dark='dark')\n      template(v-for='item of items')\n        v-list-item(\n          v-if='item.k === `link`'\n          :href='item.t'\n          :target='item.y === `externalblank` ? `_blank` : `_self`'\n          :rel='item.y === `externalblank` ? `noopener` : ``'\n          )\n          v-list-item-avatar(size='24', tile)\n            v-icon(v-if='item.c.match(/fa[a-z] fa-/)', size='19') {{ item.c }}\n            v-icon(v-else) {{ item.c }}\n          v-list-item-title {{ item.l }}\n        v-divider.my-2(v-else-if='item.k === `divider`')\n        v-subheader.pl-4(v-else-if='item.k === `header`') {{ item.l }}\n    //-> Browse\n    v-list.py-2(v-else-if='currentMode === `browse`', dense, :class='color', :dark='dark')\n      template(v-if='currentParent.id > 0')\n        v-list-item(v-for='(item, idx) of parents', :key='`parent-` + item.id', @click='fetchBrowseItems(item)', style='min-height: 30px;')\n          v-list-item-avatar(size='18', :style='`padding-left: ` + (idx * 8) + `px; width: auto; margin: 0 5px 0 0;`')\n            v-icon(small) mdi-folder-open\n          v-list-item-title {{ item.title }}\n        v-divider.mt-2\n        v-list-item.mt-2(v-if='currentParent.pageId > 0', :href='`/` + currentParent.locale + `/` + currentParent.path', :key='`directorypage-` + currentParent.id', :input-value='path === currentParent.path')\n          v-list-item-avatar(size='24')\n            v-icon mdi-text-box\n          v-list-item-title {{ currentParent.title }}\n        v-subheader.pl-4 {{$t('common:sidebar.currentDirectory')}}\n      template(v-for='item of currentItems')\n        v-list-item(v-if='item.isFolder', :key='`childfolder-` + item.id', @click='fetchBrowseItems(item)')\n          v-list-item-avatar(size='24')\n            v-icon mdi-folder\n          v-list-item-title {{ item.title }}\n        v-list-item(v-else, :href='`/` + item.locale + `/` + item.path', :key='`childpage-` + item.id', :input-value='path === item.path')\n          v-list-item-avatar(size='24')\n            v-icon mdi-text-box\n          v-list-item-title {{ item.title }}\n</template>\n\n<script>\nimport _ from 'lodash'\nimport gql from 'graphql-tag'\nimport { get } from 'vuex-pathify'\n\n/* global siteLangs */\n\nexport default {\n  props: {\n    color: {\n      type: String,\n      default: 'primary'\n    },\n    dark: {\n      type: Boolean,\n      default: true\n    },\n    items: {\n      type: Array,\n      default: () => []\n    },\n    navMode: {\n      type: String,\n      default: 'MIXED'\n    }\n  },\n  data() {\n    return {\n      currentMode: 'custom',\n      currentItems: [],\n      currentParent: {\n        id: 0,\n        title: '/ (root)'\n      },\n      parents: [],\n      loadedCache: []\n    }\n  },\n  computed: {\n    path: get('page/path'),\n    locale: get('page/locale')\n  },\n  methods: {\n    switchMode (mode) {\n      this.currentMode = mode\n      window.localStorage.setItem('navPref', mode)\n      if (mode === `browse` && this.loadedCache.length < 1) {\n        this.loadFromCurrentPath()\n      }\n    },\n    async fetchBrowseItems (item) {\n      this.$store.commit(`loadingStart`, 'browse-load')\n      if (!item) {\n        item = this.currentParent\n      }\n\n      if (this.loadedCache.indexOf(item.id) < 0) {\n        this.currentItems = []\n      }\n\n      if (item.id === 0) {\n        this.parents = []\n      } else {\n        const flushRightIndex = _.findIndex(this.parents, ['id', item.id])\n        if (flushRightIndex >= 0) {\n          this.parents = _.take(this.parents, flushRightIndex)\n        }\n        if (this.parents.length < 1) {\n          this.parents.push(this.currentParent)\n        }\n        this.parents.push(item)\n      }\n\n      this.currentParent = item\n\n      const resp = await this.$apollo.query({\n        query: gql`\n          query ($parent: Int, $locale: String!) {\n            pages {\n              tree(parent: $parent, mode: ALL, locale: $locale) {\n                id\n                path\n                title\n                isFolder\n                pageId\n                parent\n                locale\n              }\n            }\n          }\n        `,\n        fetchPolicy: 'cache-first',\n        variables: {\n          parent: item.id,\n          locale: this.locale\n        }\n      })\n      this.loadedCache = _.union(this.loadedCache, [item.id])\n      this.currentItems = _.get(resp, 'data.pages.tree', [])\n      this.$store.commit(`loadingStop`, 'browse-load')\n    },\n    async loadFromCurrentPath() {\n      this.$store.commit(`loadingStart`, 'browse-load')\n      const resp = await this.$apollo.query({\n        query: gql`\n          query ($path: String, $locale: String!) {\n            pages {\n              tree(path: $path, mode: ALL, locale: $locale, includeAncestors: true) {\n                id\n                path\n                title\n                isFolder\n                pageId\n                parent\n                locale\n              }\n            }\n          }\n        `,\n        fetchPolicy: 'cache-first',\n        variables: {\n          path: this.path,\n          locale: this.locale\n        }\n      })\n      const items = _.get(resp, 'data.pages.tree', [])\n      const curPage = _.find(items, ['pageId', this.$store.get('page/id')])\n      if (!curPage) {\n        console.warn('Could not find current page in page tree listing!')\n        return\n      }\n\n      let curParentId = curPage.parent\n      let invertedAncestors = []\n      while (curParentId) {\n        const curParent = _.find(items, ['id', curParentId])\n        if (!curParent) {\n          break\n        }\n        invertedAncestors.push(curParent)\n        curParentId = curParent.parent\n      }\n\n      this.parents = [this.currentParent, ...invertedAncestors.reverse()]\n      this.currentParent = _.last(this.parents)\n\n      this.loadedCache = [curPage.parent]\n      this.currentItems = _.filter(items, ['parent', curPage.parent])\n      this.$store.commit(`loadingStop`, 'browse-load')\n    },\n    goHome () {\n      window.location.assign(siteLangs.length > 0 ? `/${this.locale}/home` : '/')\n    }\n  },\n  mounted () {\n    this.currentParent.title = `/ ${this.$t('common:sidebar.root')}`\n    if (this.navMode === 'TREE') {\n      this.currentMode = 'browse'\n    } else if (this.navMode === 'STATIC') {\n      this.currentMode = 'custom'\n    } else {\n      this.currentMode = window.localStorage.getItem('navPref') || 'custom'\n    }\n    if (this.currentMode === 'browse') {\n      this.loadFromCurrentPath()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "client/themes/default/components/page.vue",
    "content": "<template lang=\"pug\">\n  v-app(v-scroll='upBtnScroll', :dark='$vuetify.theme.dark', :class='$vuetify.rtl ? `is-rtl` : `is-ltr`')\n    nav-header(v-if='!printView')\n    v-navigation-drawer(\n      v-if='navMode !== `NONE` && !printView'\n      :class='$vuetify.theme.dark ? `grey darken-4-d4` : `primary`'\n      dark\n      app\n      clipped\n      mobile-breakpoint='600'\n      :temporary='$vuetify.breakpoint.smAndDown'\n      v-model='navShown'\n      :right='$vuetify.rtl'\n      )\n      vue-scroll(:ops='scrollStyle')\n        nav-sidebar(:color='$vuetify.theme.dark ? `grey darken-4-d4` : `primary`', :items='sidebarDecoded', :nav-mode='navMode')\n\n    v-fab-transition(v-if='navMode !== `NONE`')\n      v-btn(\n        fab\n        color='primary'\n        fixed\n        bottom\n        :right='$vuetify.rtl'\n        :left='!$vuetify.rtl'\n        small\n        @click='navShown = !navShown'\n        v-if='$vuetify.breakpoint.mdAndDown'\n        v-show='!navShown'\n        )\n        v-icon mdi-menu\n\n    v-main(ref='content')\n      template(v-if='path !== `home`')\n        v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-d3` : `grey lighten-3`', flat, dense, v-if='$vuetify.breakpoint.smAndUp')\n          //- v-btn.pl-0(v-if='$vuetify.breakpoint.xsOnly', flat, @click='toggleNavigation')\n          //-   v-icon(color='grey darken-2', left) menu\n          //-   span Navigation\n          v-breadcrumbs.breadcrumbs-nav.pl-0(\n            :items='breadcrumbs'\n            divider='/'\n            )\n            template(slot='item', slot-scope='props')\n              v-icon(v-if='props.item.path === \"/\"', small, @click='goHome') mdi-home\n              v-btn.ma-0(v-else, :href='props.item.path', small, text) {{props.item.name}}\n          template(v-if='!isPublished')\n            v-spacer\n            .caption.red--text {{$t('common:page.unpublished')}}\n            status-indicator.ml-3(negative, pulse)\n        v-divider\n      v-container.grey.pa-0(fluid, :class='$vuetify.theme.dark ? `darken-4-l3` : `lighten-4`')\n        v-row.page-header-section(no-gutters, align-content='center', style='height: 90px;')\n          v-col.page-col-content.is-page-header(\n            :offset-xl='tocPosition === `left` ? 2 : 0'\n            :offset-lg='tocPosition === `left` ? 3 : 0'\n            :xl='tocPosition === `right` ? 10 : false'\n            :lg='tocPosition === `right` ? 9 : false'\n            style='margin-top: auto; margin-bottom: auto;'\n            :class='$vuetify.rtl ? `pr-4` : `pl-4`'\n            )\n            .page-header-headings\n              .headline.grey--text(:class='$vuetify.theme.dark ? `text--lighten-2` : `text--darken-3`') {{title}}\n              .caption.grey--text.text--darken-1 {{description}}\n            .page-edit-shortcuts(\n              v-if='editShortcutsObj.editMenuBar'\n              :class='tocPosition === `right` ? `is-right` : ``'\n              )\n              v-btn(\n                v-if='editShortcutsObj.editMenuBtn'\n                @click='pageEdit'\n                depressed\n                small\n                )\n                v-icon.mr-2(small) mdi-pencil\n                span.text-none {{$t(`common:actions.edit`)}}\n              v-btn(\n                v-if='editShortcutsObj.editMenuExternalBtn'\n                :href='editMenuExternalUrl'\n                target='_blank'\n                depressed\n                small\n                )\n                v-icon.mr-2(small) {{ editShortcutsObj.editMenuExternalIcon }}\n                span.text-none {{$t(`common:page.editExternal`, { name: editShortcutsObj.editMenuExternalName })}}\n      v-divider\n      v-container.pl-5.pt-4(fluid, grid-list-xl)\n        v-layout(row)\n          v-flex.page-col-sd(\n            v-if='tocPosition !== `off` && $vuetify.breakpoint.lgAndUp'\n            :order-xs1='tocPosition !== `right`'\n            :order-xs2='tocPosition === `right`'\n            lg3\n            xl2\n            )\n            v-card.page-toc-card.mb-5(v-if='tocDecoded.length')\n              .overline.pa-5.pb-0(:class='$vuetify.theme.dark ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}}\n              v-list.pb-3(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``')\n                template(v-for='(tocItem, tocIdx) in tocDecoded')\n                  v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)')\n                    v-icon(color='grey', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}\n                    v-list-item-title.px-3 {{tocItem.title}}\n                  //- v-divider(v-if='tocIdx < toc.length - 1 || tocItem.children.length')\n                  template(v-for='tocSubItem in tocItem.children')\n                    v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)')\n                      v-icon.px-3(color='grey lighten-1', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}\n                      v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}\n                    //- v-divider(inset, v-if='tocIdx < toc.length - 1')\n\n            v-card.page-tags-card.mb-5(v-if='tags.length > 0')\n              .pa-5\n                .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}}\n                v-chip.mr-1.mb-1(\n                  label\n                  :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'\n                  v-for='(tag, idx) in tags'\n                  :href='`/t/` + tag.tag'\n                  :key='`tag-` + tag.tag'\n                  )\n                  v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', left, small) mdi-tag\n                  span(:class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`') {{tag.title}}\n                v-chip.mr-1.mb-1(\n                  label\n                  :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'\n                  :href='`/t/` + tags.map(t => t.tag).join(`/`)'\n                  :aria-label='$t(`common:page.tagsMatching`)'\n                  )\n                  v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple\n\n            v-card.page-comments-card.mb-5(v-if='commentsEnabled && commentsPerms.read')\n              .pa-5\n                .overline.pb-2.blue-grey--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : `text--darken-2`')\n                  span {{$t('common:comments.sdTitle')}}\n                  //- v-spacer\n                  //- v-chip.text-center(\n                  //-   v-if='!commentsExternal'\n                  //-   label\n                  //-   x-small\n                  //-   :color='$vuetify.theme.dark ? `blue-grey darken-3` : `blue-grey darken-2`'\n                  //-   dark\n                  //-   style='min-width: 50px; justify-content: center;'\n                  //-   )\n                  //-   span {{commentsCount}}\n                .d-flex\n                  v-btn.text-none(\n                    @click='goToComments()'\n                    :color='$vuetify.theme.dark ? `blue-grey` : `blue-grey darken-2`'\n                    outlined\n                    style='flex: 1 1 100%;'\n                    small\n                    )\n                    span.blue-grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') {{$t('common:comments.viewDiscussion')}}\n                  v-tooltip(right, v-if='commentsPerms.write')\n                    template(v-slot:activator='{ on }')\n                      v-btn.ml-2(\n                        @click='goToComments(true)'\n                        v-on='on'\n                        outlined\n                        small\n                        :color='$vuetify.theme.dark ? `blue-grey` : `blue-grey darken-2`'\n                        :aria-label='$t(`common:comments.newComment`)'\n                        )\n                        v-icon(:color='$vuetify.theme.dark ? `blue-grey lighten-1` : `blue-grey darken-2`', dense) mdi-comment-plus\n                    span {{$t('common:comments.newComment')}}\n\n            v-card.page-author-card.mb-5\n              .pa-5\n                .overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')\n                  span {{$t('common:page.lastEditedBy')}}\n                  v-spacer\n                  v-tooltip(right, v-if='isAuthenticated')\n                    template(v-slot:activator='{ on }')\n                      v-btn.btn-animate-edit(\n                        icon\n                        :href='\"/h/\" + locale + \"/\" + path'\n                        v-on='on'\n                        x-small\n                        v-if='hasReadHistoryPermission'\n                        :aria-label='$t(`common:header.history`)'\n                        )\n                        v-icon(color='indigo', dense) mdi-history\n                    span {{$t('common:header.history')}}\n                .page-author-card-name.body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }}\n                .page-author-card-date.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}\n\n            //- v-card.mb-5\n            //-   .pa-5\n            //-     .overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating\n            //-     .text-center\n            //-       v-rating(\n            //-         v-model='rating'\n            //-         color='yellow darken-3'\n            //-         background-color='grey lighten-1'\n            //-         half-increments\n            //-         hover\n            //-       )\n            //-       .caption.grey--text 5 votes\n\n            v-card.page-shortcuts-card(flat)\n              v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-d3` : `grey lighten-3`', flat, dense)\n                v-spacer\n                //- v-tooltip(bottom)\n                //-   template(v-slot:activator='{ on }')\n                //-     v-btn(icon, tile, v-on='on', :aria-label='$t(`common:page.bookmark`)'): v-icon(color='grey') mdi-bookmark\n                //-   span {{$t('common:page.bookmark')}}\n                v-menu(offset-y, bottom, min-width='300')\n                  template(v-slot:activator='{ on: menu }')\n                    v-tooltip(bottom)\n                      template(v-slot:activator='{ on: tooltip }')\n                        v-btn(icon, tile, v-on='{ ...menu, ...tooltip }', :aria-label='$t(`common:page.share`)'): v-icon(color='grey') mdi-share-variant\n                      span {{$t('common:page.share')}}\n                  social-sharing(\n                    :url='pageUrl'\n                    :title='title'\n                    :description='description'\n                  )\n                v-tooltip(bottom)\n                  template(v-slot:activator='{ on }')\n                    v-btn(icon, tile, v-on='on', @click='print', :aria-label='$t(`common:page.printFormat`)')\n                      v-icon(:color='printView ? `primary` : `grey`') mdi-printer\n                  span {{$t('common:page.printFormat')}}\n                v-spacer\n\n          v-flex.page-col-content(\n            xs12\n            :lg9='tocPosition !== `off`'\n            :xl10='tocPosition !== `off`'\n            :order-xs1='tocPosition === `right`'\n            :order-xs2='tocPosition !== `right`'\n            )\n            v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasAnyPagePermissions && editShortcutsObj.editFab')\n              template(v-slot:activator='{ on: onEditActivator }')\n                v-speed-dial(\n                  v-model='pageEditFab'\n                  direction='top'\n                  open-on-hover\n                  transition='scale-transition'\n                  bottom\n                  :right='!$vuetify.rtl'\n                  :left='$vuetify.rtl'\n                  fixed\n                  dark\n                  )\n                  template(v-slot:activator)\n                    v-btn.btn-animate-edit(\n                      fab\n                      color='primary'\n                      v-model='pageEditFab'\n                      @click='pageEdit'\n                      v-on='onEditActivator'\n                      :disabled='!hasWritePagesPermission'\n                      :aria-label='$t(`common:page.editPage`)'\n                      )\n                      v-icon mdi-pencil\n                  v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasReadHistoryPermission')\n                    template(v-slot:activator='{ on }')\n                      v-btn(\n                        fab\n                        small\n                        color='white'\n                        light\n                        v-on='on'\n                        @click='pageHistory'\n                        )\n                        v-icon(size='20') mdi-history\n                    span {{$t('common:header.history')}}\n                  v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasReadSourcePermission')\n                    template(v-slot:activator='{ on }')\n                      v-btn(\n                        fab\n                        small\n                        color='white'\n                        light\n                        v-on='on'\n                        @click='pageSource'\n                        )\n                        v-icon(size='20') mdi-code-tags\n                    span {{$t('common:header.viewSource')}}\n                  v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasWritePagesPermission')\n                    template(v-slot:activator='{ on }')\n                      v-btn(\n                        fab\n                        small\n                        color='white'\n                        light\n                        v-on='on'\n                        @click='pageConvert'\n                        )\n                        v-icon(size='20') mdi-lightning-bolt\n                    span {{$t('common:header.convert')}}\n                  v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasWritePagesPermission')\n                    template(v-slot:activator='{ on }')\n                      v-btn(\n                        fab\n                        small\n                        color='white'\n                        light\n                        v-on='on'\n                        @click='pageDuplicate'\n                        )\n                        v-icon(size='20') mdi-content-duplicate\n                    span {{$t('common:header.duplicate')}}\n                  v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasManagePagesPermission')\n                    template(v-slot:activator='{ on }')\n                      v-btn(\n                        fab\n                        small\n                        color='white'\n                        light\n                        v-on='on'\n                        @click='pageMove'\n                        )\n                        v-icon(size='20') mdi-content-save-move-outline\n                    span {{$t('common:header.move')}}\n                  v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasDeletePagesPermission')\n                    template(v-slot:activator='{ on }')\n                      v-btn(\n                        fab\n                        dark\n                        small\n                        color='red'\n                        v-on='on'\n                        @click='pageDelete'\n                        )\n                        v-icon(size='20') mdi-trash-can-outline\n                    span {{$t('common:header.delete')}}\n              span {{$t('common:page.editPage')}}\n            v-alert.mb-5(v-if='!isPublished', color='red', outlined, icon='mdi-minus-circle', dense)\n              .caption {{$t('common:page.unpublishedWarning')}}\n            .contents(ref='container')\n              slot(name='contents')\n            .comments-container#discussion(v-if='commentsEnabled && commentsPerms.read && !printView')\n              .comments-header\n                v-icon.mr-2(dark) mdi-comment-text-outline\n                span {{$t('common:comments.title')}}\n              .comments-main\n                slot(name='comments')\n    nav-footer\n    notify\n    search-results\n    v-fab-transition\n      v-btn(\n        v-if='upBtnShown'\n        fab\n        fixed\n        bottom\n        :right='$vuetify.rtl'\n        :left='!$vuetify.rtl'\n        small\n        :depressed='this.$vuetify.breakpoint.mdAndUp'\n        @click='$vuetify.goTo(0, scrollOpts)'\n        color='primary'\n        dark\n        :style='upBtnPosition'\n        :aria-label='$t(`common:actions.returnToTop`)'\n        )\n        v-icon mdi-arrow-up\n</template>\n\n<script>\nimport { StatusIndicator } from 'vue-status-indicator'\nimport Tabset from './tabset.vue'\nimport NavSidebar from './nav-sidebar.vue'\nimport Prism from 'prismjs'\nimport mermaid from 'mermaid'\nimport { get, sync } from 'vuex-pathify'\nimport _ from 'lodash'\nimport ClipboardJS from 'clipboard'\nimport Vue from 'vue'\n\n/* global siteLangs */\n\nVue.component('Tabset', Tabset)\n\nPrism.plugins.autoloader.languages_path = '/_assets/js/prism/'\nPrism.plugins.NormalizeWhitespace.setDefaults({\n  'remove-trailing': true,\n  'remove-indent': true,\n  'left-trim': true,\n  'right-trim': true,\n  'remove-initial-line-feed': true,\n  'tabs-to-spaces': 2\n})\nPrism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {\n  let linkCopy = document.createElement('button')\n  linkCopy.textContent = 'Copy'\n\n  const clip = new ClipboardJS(linkCopy, {\n    text: () => { return env.code }\n  })\n\n  clip.on('success', () => {\n    linkCopy.textContent = 'Copied!'\n    resetClipboardText()\n  })\n  clip.on('error', () => {\n    linkCopy.textContent = 'Press Ctrl+C to copy'\n    resetClipboardText()\n  })\n\n  return linkCopy\n\n  function resetClipboardText() {\n    setTimeout(() => {\n      linkCopy.textContent = 'Copy'\n    }, 5000)\n  }\n})\n\nexport default {\n  components: {\n    NavSidebar,\n    StatusIndicator\n  },\n  props: {\n    pageId: {\n      type: Number,\n      default: 0\n    },\n    locale: {\n      type: String,\n      default: 'en'\n    },\n    path: {\n      type: String,\n      default: 'home'\n    },\n    title: {\n      type: String,\n      default: 'Untitled Page'\n    },\n    description: {\n      type: String,\n      default: ''\n    },\n    createdAt: {\n      type: String,\n      default: ''\n    },\n    updatedAt: {\n      type: String,\n      default: ''\n    },\n    tags: {\n      type: Array,\n      default: () => ([])\n    },\n    authorName: {\n      type: String,\n      default: 'Unknown'\n    },\n    authorId: {\n      type: Number,\n      default: 0\n    },\n    editor: {\n      type: String,\n      default: ''\n    },\n    isPublished: {\n      type: Boolean,\n      default: false\n    },\n    toc: {\n      type: String,\n      default: ''\n    },\n    sidebar: {\n      type: String,\n      default: ''\n    },\n    navMode: {\n      type: String,\n      default: 'MIXED'\n    },\n    commentsEnabled: {\n      type: Boolean,\n      default: false\n    },\n    effectivePermissions: {\n      type: String,\n      default: ''\n    },\n    commentsExternal: {\n      type: Boolean,\n      default: false\n    },\n    editShortcuts: {\n      type: String,\n      default: ''\n    },\n    filename: {\n      type: String,\n      default: ''\n    }\n  },\n  data() {\n    return {\n      locales: siteLangs,\n      navShown: false,\n      navExpanded: false,\n      upBtnShown: false,\n      pageEditFab: false,\n      scrollOpts: {\n        duration: 1500,\n        offset: 0,\n        easing: 'easeInOutCubic'\n      },\n      scrollStyle: {\n        vuescroll: {},\n        scrollPanel: {\n          initialScrollX: 0.01, // fix scrollbar not disappearing on load\n          scrollingX: false,\n          speed: 50\n        },\n        rail: {\n          gutterOfEnds: '2px'\n        },\n        bar: {\n          onlyShowBarOnScroll: false,\n          background: '#42A5F5',\n          hoverStyle: {\n            background: '#64B5F6'\n          }\n        }\n      },\n      winWidth: 0\n    }\n  },\n  computed: {\n    isAuthenticated: get('user/authenticated'),\n    commentsCount: get('page/commentsCount'),\n    commentsPerms: get('page/effectivePermissions@comments'),\n    editShortcutsObj: get('page/editShortcuts'),\n    rating: {\n      get () {\n        return 3.5\n      },\n      set (val) {\n\n      }\n    },\n    breadcrumbs() {\n      return [{ path: '/', name: 'Home' }].concat(\n        _.reduce(this.path.split('/'), (result, value) => {\n          result.push({\n            path: _.get(_.last(result), 'path', this.locales.length > 0 ? `/${this.locale}` : '') + `/${value}`,\n            name: value\n          })\n          return result\n        }, []))\n    },\n    pageUrl () { return window.location.href },\n    upBtnPosition () {\n      if (this.$vuetify.breakpoint.mdAndUp) {\n        return this.$vuetify.rtl ? `right: 235px;` : `left: 235px;`\n      } else {\n        return this.$vuetify.rtl ? `right: 65px;` : `left: 65px;`\n      }\n    },\n    sidebarDecoded () {\n      return JSON.parse(Buffer.from(this.sidebar, 'base64').toString())\n    },\n    tocDecoded () {\n      return JSON.parse(Buffer.from(this.toc, 'base64').toString())\n    },\n    tocPosition: get('site/tocPosition'),\n    hasAdminPermission: get('page/effectivePermissions@system.manage'),\n    hasWritePagesPermission: get('page/effectivePermissions@pages.write'),\n    hasManagePagesPermission: get('page/effectivePermissions@pages.manage'),\n    hasDeletePagesPermission: get('page/effectivePermissions@pages.delete'),\n    hasReadSourcePermission: get('page/effectivePermissions@source.read'),\n    hasReadHistoryPermission: get('page/effectivePermissions@history.read'),\n    hasAnyPagePermissions () {\n      return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission ||\n        this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission\n    },\n    printView: sync('site/printView'),\n    editMenuExternalUrl () {\n      if (this.editShortcutsObj.editMenuBar && this.editShortcutsObj.editMenuExternalBtn) {\n        return this.editShortcutsObj.editMenuExternalUrl.replace('{filename}', this.filename)\n      } else {\n        return ''\n      }\n    }\n  },\n  created() {\n    this.$store.set('page/authorId', this.authorId)\n    this.$store.set('page/authorName', this.authorName)\n    this.$store.set('page/createdAt', this.createdAt)\n    this.$store.set('page/description', this.description)\n    this.$store.set('page/isPublished', this.isPublished)\n    this.$store.set('page/id', this.pageId)\n    this.$store.set('page/locale', this.locale)\n    this.$store.set('page/path', this.path)\n    this.$store.set('page/tags', this.tags)\n    this.$store.set('page/title', this.title)\n    this.$store.set('page/editor', this.editor)\n    this.$store.set('page/updatedAt', this.updatedAt)\n    if (this.effectivePermissions) {\n      this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))\n    }\n    if (this.editShortcuts) {\n      this.$store.set('page/editShortcuts', JSON.parse(Buffer.from(this.editShortcuts, 'base64').toString()))\n    }\n\n    this.$store.set('page/mode', 'view')\n  },\n  mounted () {\n    if (this.$vuetify.theme.dark) {\n      this.scrollStyle.bar.background = '#424242'\n    }\n\n    // -> Check side navigation visibility\n    this.handleSideNavVisibility()\n    window.addEventListener('resize', _.debounce(() => {\n      this.handleSideNavVisibility()\n    }, 500))\n\n    // -> Highlight Code Blocks\n    Prism.highlightAllUnder(this.$refs.container)\n\n    // -> Render Mermaid diagrams\n    mermaid.mermaidAPI.initialize({\n      startOnLoad: true,\n      theme: this.$vuetify.theme.dark ? `dark` : `default`\n    })\n\n    // -> Handle anchor scrolling\n    if (window.location.hash && window.location.hash.length > 1) {\n      if (document.readyState === 'complete') {\n        this.$nextTick(() => {\n          this.$vuetify.goTo(decodeURIComponent(window.location.hash), this.scrollOpts)\n        })\n      } else {\n        window.addEventListener('load', () => {\n          this.$vuetify.goTo(decodeURIComponent(window.location.hash), this.scrollOpts)\n        })\n      }\n    }\n\n    // -> Handle anchor links within the page contents\n    this.$nextTick(() => {\n      this.$refs.container.querySelectorAll(`a[href^=\"#\"], a[href^=\"${window.location.href.replace(window.location.hash, '')}#\"]`).forEach(el => {\n        el.onclick = ev => {\n          ev.preventDefault()\n          ev.stopPropagation()\n          this.$vuetify.goTo(decodeURIComponent(ev.currentTarget.hash), this.scrollOpts)\n        }\n      })\n\n      window.boot.notify('page-ready')\n    })\n  },\n  methods: {\n    goHome () {\n      if (this.locales && this.locales.length > 0) {\n        window.location.assign(`/${this.locale}/home`)\n      } else {\n        window.location.assign('/')\n      }\n    },\n    toggleNavigation () {\n      this.navOpen = !this.navOpen\n    },\n    upBtnScroll () {\n      const scrollOffset = window.pageYOffset || document.documentElement.scrollTop\n      this.upBtnShown = scrollOffset > window.innerHeight * 0.33\n    },\n    print () {\n      if (this.printView) {\n        this.printView = false\n      } else {\n        this.printView = true\n        this.$nextTick(() => {\n          window.print()\n        })\n      }\n    },\n    pageEdit () {\n      this.$root.$emit('pageEdit')\n    },\n    pageHistory () {\n      this.$root.$emit('pageHistory')\n    },\n    pageSource () {\n      this.$root.$emit('pageSource')\n    },\n    pageConvert () {\n      this.$root.$emit('pageConvert')\n    },\n    pageDuplicate () {\n      this.$root.$emit('pageDuplicate')\n    },\n    pageMove () {\n      this.$root.$emit('pageMove')\n    },\n    pageDelete () {\n      this.$root.$emit('pageDelete')\n    },\n    handleSideNavVisibility () {\n      if (window.innerWidth === this.winWidth) { return }\n      this.winWidth = window.innerWidth\n      if (this.$vuetify.breakpoint.mdAndUp) {\n        this.navShown = true\n      } else {\n        this.navShown = false\n      }\n    },\n    goToComments (focusNewComment = false) {\n      this.$vuetify.goTo('#discussion', this.scrollOpts)\n      if (focusNewComment) {\n        document.querySelector('#discussion-new').focus()\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n\n.breadcrumbs-nav {\n  .v-btn {\n    min-width: 0;\n    &__content {\n      text-transform: none;\n    }\n  }\n  .v-breadcrumbs__divider:nth-child(2n) {\n    padding: 0 6px;\n  }\n  .v-breadcrumbs__divider:nth-child(2) {\n    padding: 0 6px 0 12px;\n  }\n}\n\n.page-col-sd {\n  margin-top: -90px;\n  align-self: flex-start;\n  position: sticky;\n  top: 64px;\n  max-height: calc(100vh - 64px);\n  overflow-y: auto;\n  -ms-overflow-style: none;\n}\n\n.page-col-sd::-webkit-scrollbar {\n  display: none;\n}\n\n.page-header-section {\n  position: relative;\n\n  > .is-page-header {\n    position: relative;\n  }\n\n  .page-header-headings {\n    min-height: 52px;\n    display: flex;\n    justify-content: center;\n    flex-direction: column;\n  }\n\n  .page-edit-shortcuts {\n    position: absolute;\n    bottom: -33px;\n    right: 10px;\n\n    .v-btn {\n      border-right: 1px solid #DDD !important;\n      border-bottom: 1px solid #DDD !important;\n      border-radius: 0;\n      color: #777;\n      background-color: #FFF !important;\n\n      @at-root .theme--dark & {\n        background-color: #222 !important;\n        border-right-color: #444 !important;\n        border-bottom-color: #444 !important;\n        color: #CCC;\n      }\n\n      .v-icon {\n        color: mc('blue', '700');\n      }\n\n      &:first-child {\n        border-top-left-radius: 5px;\n        border-bottom-left-radius: 5px;\n      }\n\n      &:last-child {\n        border-top-right-radius: 5px;\n        border-bottom-right-radius: 5px;\n      }\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "client/themes/default/components/tabset.vue",
    "content": "<template lang=\"pug\">\n  .tabset.elevation-2\n    ul.tabset-tabs(ref='tabs', role='tablist')\n      slot(name='tabs')\n    .tabset-content(ref='content')\n      slot(name='content')\n</template>\n\n<script>\nimport { customAlphabet } from 'nanoid/non-secure'\n\nconst nanoid = customAlphabet('1234567890abcdef', 10)\n\nexport default {\n  data() {\n    return {\n      currentTab: 0\n    }\n  },\n  watch: {\n    currentTab (newValue, oldValue) {\n      this.setActiveTab()\n    }\n  },\n  methods: {\n    setActiveTab () {\n      this.$refs.tabs.childNodes.forEach((node, idx) => {\n        if (idx === this.currentTab) {\n          node.className = 'is-active'\n          node.setAttribute('aria-selected', 'true')\n        } else {\n          node.className = ''\n          node.setAttribute('aria-selected', 'false')\n        }\n      })\n      this.$refs.content.childNodes.forEach((node, idx) => {\n        if (idx === this.currentTab) {\n          node.className = 'tabset-panel is-active'\n          node.removeAttribute('hidden')\n        } else {\n          node.className = 'tabset-panel'\n          node.setAttribute('hidden', '')\n        }\n      })\n    }\n  },\n  mounted () {\n    // Handle scroll to header on load within hidden tab content\n    if (window.location.hash && window.location.hash.length > 1) {\n      const headerId = decodeURIComponent(window.location.hash)\n      let foundIdx = -1\n      this.$refs.content.childNodes.forEach((node, idx) => {\n        if (node.querySelector(headerId)) {\n          foundIdx = idx\n        }\n      })\n      if (foundIdx >= 0) {\n        this.currentTab = foundIdx\n      }\n    }\n\n    this.setActiveTab()\n\n    const tabRefId = nanoid()\n\n    this.$refs.tabs.childNodes.forEach((node, idx) => {\n      node.setAttribute('id', `${tabRefId}-${idx}`)\n      node.setAttribute('role', 'tab')\n      node.setAttribute('aria-controls', `${tabRefId}-${idx}-tab`)\n      node.setAttribute('tabindex', '0')\n      node.addEventListener('click', ev => {\n        this.currentTab = [].indexOf.call(ev.target.parentNode.children, ev.target)\n      })\n      node.addEventListener('keydown', ev => {\n        if (ev.key === 'ArrowLeft' && idx > 0) {\n          this.currentTab = idx - 1\n          this.$refs.tabs.childNodes[idx - 1].focus()\n        } else if (ev.key === 'ArrowRight' && idx < this.$refs.tabs.childNodes.length - 1) {\n          this.currentTab = idx + 1\n          this.$refs.tabs.childNodes[idx + 1].focus()\n        } else if (ev.key === 'Enter' || ev.key === ' ') {\n          this.currentTab = idx\n          node.focus()\n        } else if (ev.key === 'Home') {\n          this.currentTab = 0\n          ev.preventDefault()\n          ev.target.parentNode.children[0].focus()\n        } else if (ev.key === 'End') {\n          this.currentTab = this.$refs.tabs.childNodes.length - 1\n          ev.preventDefault()\n          ev.target.parentNode.children[this.$refs.tabs.childNodes.length - 1].focus()\n        }\n      })\n    })\n\n    this.$refs.content.childNodes.forEach((node, idx) => {\n      node.setAttribute('id', `${tabRefId}-${idx}-tab`)\n      node.setAttribute('role', 'tabpanel')\n      node.setAttribute('aria-labelledby', `${tabRefId}-${idx}`)\n      node.setAttribute('tabindex', '0')\n    })\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.tabset {\n  border-radius: 5px;\n  margin-top: 10px;\n\n  @at-root .theme--dark & {\n    background-color: #292929;\n  }\n\n  > .tabset-tabs {\n    padding-left: 0;\n    margin: 0;\n    display: flex;\n    align-items: stretch;\n    background: linear-gradient(to bottom, #FFF, #FAFAFA);\n    box-shadow: inset 0 -1px 0 0 #DDD;\n    border-radius: 5px 5px 0 0;\n    overflow: auto;\n\n    @at-root .theme--dark & {\n      background: linear-gradient(to bottom, #424242, #333);\n      box-shadow: inset 0 -1px 0 0 #555;\n    }\n\n    > li {\n      display: block;\n      padding: 16px;\n      margin-top: 0;\n      cursor: pointer;\n      transition: color 1s ease;\n      border-right: 1px solid #FFF;\n      font-size: 14px;\n      font-weight: 500;\n      margin-bottom: 1px;\n      user-select: none;\n\n      @at-root .theme--dark & {\n        border-right-color: #555;\n      }\n\n      &.is-active {\n        background-color: #FFF;\n        margin-bottom: 0;\n        padding-bottom: 17px;\n        padding-top: 13px;\n        color: mc('blue', '700');\n        border-top: 3px solid mc('blue', '700');\n\n        @at-root .theme--dark & {\n          background-color: #292929;\n          color: mc('blue', '300');\n        }\n      }\n\n      &:last-child {\n        border-right: none;\n\n        &.is-active {\n          border-right: 1px solid #EEE;\n\n          @at-root .theme--dark & {\n            border-right-color: #555;\n          }\n        }\n      }\n\n      &:hover {\n        background-color: rgba(#CCC, .1);\n\n        @at-root .theme--dark & {\n          background-color: rgba(#222, .25);\n        }\n\n        &.is-active {\n          background-color: #FFF;\n\n          @at-root .theme--dark & {\n            background-color: #292929;\n          }\n        }\n      }\n\n      & + li {\n        border-left: 1px solid #EEE;\n\n        @at-root .theme--dark & {\n          border-left-color: #222;\n        }\n      }\n    }\n  }\n\n  > .tabset-content {\n    .tabset-panel {\n      padding: 2px 16px 16px;\n      display: none;\n\n      &.is-active {\n        display: block;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "client/themes/default/js/app.js",
    "content": "/* THEME SPECIFIC JAVASCRIPT */\n"
  },
  {
    "path": "client/themes/default/scss/app.scss",
    "content": "/* THEME SPECIFIC STYLES */\n\n.v-main .contents {\n  color: mc('grey', '800');\n  padding: .5rem 0 50px;\n  position: relative;\n\n  > div > *:first-child {\n    margin-top: 0;\n  }\n\n  @at-root .theme--dark & {\n    color: mc('grey', '300');\n  }\n\n  // ---------------------------------\n  // LINKS\n  // ---------------------------------\n\n  a {\n    color: mc('blue', '700');\n\n    &.is-internal-link.is-invalid-page {\n      color: mc('red', '700');\n\n      @at-root .theme--dark & {\n        color: mc('red', '200');\n      }\n    }\n\n    &.is-external-link {\n      padding-right: 3px;\n\n      &::after {\n        font-family: 'Material Design Icons', sans-serif;\n        font-size: 24px/1;\n        padding-left: 3px;\n        display: inline-block;\n        content: '\\F03CC';\n        color: mc('grey', '500');\n        text-decoration: none;\n      }\n    }\n\n    @at-root .theme--dark & {\n      color: mc('blue', '200');\n    }\n  }\n\n  // ---------------------------------\n  // HEADERS\n  // ---------------------------------\n\n  h1, h2, h3, h4, h5, h6 {\n    position: relative;\n\n    &:first-child {\n      padding-top: 0;\n    }\n\n    &:hover {\n      .toc-anchor {\n        display: block;\n      }\n    }\n\n    .toc-anchor {\n      display: none;\n      position: absolute;\n      right: 1rem;\n      bottom: .5rem;\n      font-size: 1.25rem;\n      text-decoration: none;\n      color: mc('grey', '500');\n    }\n\n    & + h2, & + h3, & + h4, & + h5, & + h6 {\n      margin-top: 8px;\n    }\n\n    & + hr.footnotes-sep {\n      display: none;\n    }\n  }\n\n  h1 {\n    padding: 0;\n    color: mc('blue', '800');\n    margin-top: 2rem;\n    position: relative;\n\n    @at-root .theme--dark & {\n      color: mc('grey', '300');\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      bottom: 0;\n      left: 0;\n      width: 100%;\n      height: 2px;\n      background: linear-gradient(to right, mc('theme', 'primary'), rgba(mc('theme', 'primary'), 0));\n      border-radius: 3px;\n\n      @at-root .theme--dark & {\n        background: linear-gradient(to right, mc('blue', '300') 0%, mc('blue', '500') 10%, rgba(mc('blue', '900'), 0) 100%);\n      }\n\n      @at-root .is-rtl & {\n        background: linear-gradient(to left, mc('theme', 'primary'), rgba(mc('theme', 'primary'), 0));\n      }\n      @at-root .theme--dark.is-rtl & {\n        background: linear-gradient(to left, mc('grey', '600'), rgba(mc('grey', '600'), 0));\n      }\n    }\n  }\n  h2 {\n    margin: 1rem 0 0 0;\n    color: mc('grey', '800');\n    position: relative;\n\n    @at-root .theme--dark & {\n      color: mc('grey', '400');\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      bottom: 0;\n      left: 0;\n      width: 100%;\n      height: 1px;\n      background: linear-gradient(to right, mc('grey', '700'), rgba(mc('grey', '700'), 0));\n\n      @at-root .theme--dark & {\n        background: linear-gradient(to right, mc('grey', '300'), rgba(mc('grey', '700'), 0));\n      }\n\n      @at-root .is-rtl & {\n        background: linear-gradient(to left, mc('grey', '700'), rgba(mc('grey', '700'), 0));\n      }\n      @at-root .theme--dark.is-rtl & {\n        background: linear-gradient(to left, mc('grey', '300'), rgba(mc('grey', '700'), 0));\n      }\n    }\n  }\n  h3 {\n    margin: 8px 0 0 0;\n    color: mc('grey', '700');\n    position: relative;\n\n    @at-root .theme--dark & {\n      color: mc('grey', '600');\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      bottom: 0;\n      left: 0;\n      width: 100%;\n      height: 1px;\n      background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 90%);\n    }\n  }\n  h4, h5, h6 {\n    font-size: 1rem;\n    margin: 8px 0 0 0;\n    color: mc('grey', '700');\n    position: relative;\n\n    @at-root .theme--dark & {\n      color: mc('grey', '600');\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      bottom: 0;\n      left: 0;\n      width: 100%;\n      height: 1px;\n      background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 70%);\n    }\n  }\n  h5 {\n    &::after {\n      background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 50%);\n    }\n  }\n  h6 {\n    &::after {\n      background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 30%);\n    }\n  }\n\n  // ---------------------------------\n  // PARAGRAPHS\n  // ---------------------------------\n\n  p {\n    padding: 1rem 0 0 0;\n    margin: 0;\n\n    @at-root .contents > div > p:first-child {\n      padding-top: 0;\n    }\n\n    @at-root .v-application & {\n      margin-bottom: 0;\n    }\n  }\n\n  hr {\n    margin: 1rem 0;\n    height: 1px;\n    border: none;\n    background-color: mc('grey', '400');\n\n    @at-root .theme--dark & {\n      background-color: mc('grey', '700');\n    }\n  }\n\n  .emoji {\n    height: 1.25em;\n    margin: 0 1px -4px;\n  }\n\n  .text-huge {\n    font-size: 1.8em;\n  }\n  .text-big {\n    font-size: 1.4em;\n  }\n  .text-small {\n    font-size: .85em;\n  }\n  .text-tiny {\n    font-size: .7em;\n  }\n\n  blockquote {\n    padding: 0 1rem 1rem 1rem;\n    background-color: mc('blue-grey', '50');\n    border-left: 55px solid mc('blue-grey', '500');\n    border-radius: .5rem;\n    margin: 1rem 0;\n    position: relative;\n\n    @at-root .theme--dark & {\n      background-color: mc('blue-grey', '900');\n    }\n\n    &::before {\n      display: inline-block;\n      font: normal normal normal 24px/1 \"Material Design Icons\", sans-serif;\n      position: absolute;\n      margin-top: -12px;\n      top: 50%;\n      left: -38px;\n      color: rgba(255, 255, 255, .7);\n      content: \"\\F0757\";\n    }\n\n    > p:first-child .emoji {\n      margin-right: .5rem;\n    }\n\n    &.valign-center > p {\n      display: flex;\n      align-items: center;\n    }\n\n    &.is-info {\n      background-color: mc('blue', '50');\n      border-color: mc('blue', '300');\n      color: mc('blue', '900');\n\n      &::before {\n        content: \"\\F02FC\";\n      }\n\n      code:not([class^=\"language-\"]) {\n        background-color: mc('blue', '50');\n        color: mc('blue', '800');\n      }\n\n      @at-root .theme--dark & {\n        background-color: mc('blue', '900');\n        color: mc('blue', '50');\n        border-color: mc('blue', '500');\n      }\n    }\n    &.is-warning {\n      background-color: mc('orange', '50');\n      border-color: mc('orange', '300');\n      color: darken(mc('orange', '900'), 10%);\n\n      &::before {\n        content: \"\\F0026\";\n      }\n\n      code:not([class^=\"language-\"]) {\n        background-color: mc('orange', '50');\n        color: mc('orange', '800');\n      }\n\n      @at-root .theme--dark & {\n        background-color: darken(mc('orange', '900'), 5%);\n        color: mc('orange', '100');\n        border-color: mc('orange', '500');\n        box-shadow: 0 0 2px 0 mc('grey', '900');\n      }\n    }\n    &.is-danger {\n      background-color: mc('red', '50');\n      border-color: mc('red', '300');\n      color: mc('red', '900');\n\n      &::before {\n        content: \"\\F0159\";\n      }\n\n      code:not([class^=\"language-\"]) {\n        background-color: mc('red', '50');\n        color: mc('red', '800');\n      }\n\n      @at-root .theme--dark & {\n        background-color: mc('red', '900');\n        color: mc('red', '100');\n        border-color: mc('red', '500');\n      }\n    }\n    &.is-success {\n      background-color: mc('green', '50');\n      border-color: mc('green', '300');\n      color: mc('green', '900');\n\n      &::before {\n        content: \"\\F0E1E\";\n      }\n\n      code:not([class^=\"language-\"]) {\n        background-color: mc('green', '50');\n        color: mc('green', '800');\n      }\n\n      @at-root .theme--dark & {\n        background-color: mc('green', '900');\n        color: mc('green', '50');\n        border-color: mc('green', '500');\n      }\n    }\n  }\n\n  // ---------------------------------\n  // ASCIIDOC SPECIFIC\n  // ---------------------------------\n\n  .admonitionblock {\n    margin: 1rem 0;\n    position: relative;\n\n    table {\n      border: none;\n      background-color: transparent;\n      width: 100%;\n    }\n\n    td.icon {\n      border-bottom-left-radius: 7px;\n      border-top-left-radius: 7px;\n      text-align: center;\n      width: 56px;\n\n      &::before {\n        display: inline-block;\n        font: normal normal normal 24px/1 \"Material Design Icons\", sans-serif !important;\n      }\n    }\n\n    td.content {\n      border-bottom-right-radius: 7px;\n      border-top-right-radius: 7px;\n    }\n\n    &.note {\n      td.icon {\n        background-color: mc('blue', '300');\n        color: mc('blue', '50');\n        &::before {\n          content: \"\\F02FC\";\n        }\n      }\n      td.content {\n        color: darken(mc('blue', '900'), 10%);\n        background-color: mc('blue', '50');\n\n        @at-root .theme--dark & {\n          background-color: mc('blue', '900');\n          color: mc('blue', '50');\n        }\n      }\n    }\n    &.tip {\n      td.icon {\n        background-color: mc('green', '300');\n        color: mc('green', '50');\n        &::before {\n          content: \"\\F0335\";\n        }\n      }\n      td.content {\n        color: darken(mc('green', '900'), 10%);\n        background-color: mc('green', '50');\n\n        @at-root .theme--dark & {\n          background-color: mc('green', '900');\n          color: mc('green', '50');\n        }\n      }\n    }\n    &.warning {\n      background-color: transparent !important;\n\n      td.icon {\n        background-color: mc('orange', '300');\n        color: #FFF;\n        &::before {\n          content: \"\\F0026\";\n        }\n      }\n      td.content {\n        color: darken(mc('orange', '900'), 10%);\n        background-color: mc('orange', '50');\n\n        @at-root .theme--dark & {\n          background-color: darken(mc('orange', '900'), 5%);\n          color: mc('orange', '100');\n        }\n      }\n    }\n    &.caution {\n      td.icon {\n        background-color: mc('purple', '300');\n        color: mc('purple', '50');\n        &::before {\n          content: \"\\f0238\";\n        }\n      }\n      td.content {\n        color: darken(mc('purple', '900'), 10%);\n        background-color: mc('purple', '50');\n\n        @at-root .theme--dark & {\n          background-color: mc('purple', '900');\n          color: mc('purple', '100');\n        }\n      }\n    }\n    &.important {\n      td.icon {\n        background-color: mc('red', '300');\n        color: mc('red', '50');\n        &::before {\n          content: \"\\F0159\";\n        }\n      }\n      td.content {\n        color: darken(mc('red', '900'), 10%);\n        background-color: mc('red', '50');\n\n        @at-root .theme--dark & {\n          background-color: mc('red', '900');\n          color: mc('red', '100');\n        }\n      }\n    }\n  }\n\n  .exampleblock {\n    > .title {\n      font-style: italic;\n      font-size: 1rem !important;\n      color: #7a2717;\n\n      @at-root .theme--dark & {\n        color: mc('brown', '300');\n      }\n    }\n    > .content {\n      border: 1px solid mc('grey', '200');\n      border-radius: 7px;\n      margin-bottom: 12px;\n      padding: 16px;\n    }\n  }\n  // ---------------------------------\n  // LISTS\n  // ---------------------------------\n\n  ol, ul:not(.tabset-tabs) {\n    padding-top: 1rem;\n    width: 100%;\n\n    @at-root .is-rtl & {\n      padding-left: 0;\n      padding-right: 1rem;\n    }\n\n    li > ul, li > ol {\n      padding-top: .5rem;\n      padding-left: 1rem;\n\n      @at-root .is-rtl & {\n        padding-left: 0;\n        padding-right: 1rem;\n      }\n    }\n\n    li + li {\n      margin-top: .5rem;\n    }\n\n    &.links-list {\n      padding-left: 0;\n      list-style-type: none;\n\n      @at-root .is-rtl & {\n        padding-right: 0;\n      }\n\n      li {\n        background-color: mc('grey', '50');\n        background-image: linear-gradient(to bottom, #FFF, mc('grey', '50'));\n        border-right: 1px solid mc('grey', '200');\n        border-bottom: 1px solid mc('grey', '200');\n        border-left: 5px solid mc('grey', '300');\n        box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);\n        padding: 1rem;\n        border-radius: 5px;\n        font-weight: 500;\n\n        @at-root .is-rtl & {\n          border-left-width: 1px;\n          border-right-width: 5px;\n        }\n\n        &:hover {\n          background-image: linear-gradient(to bottom, #FFF, lighten(mc('blue', '50'), 4%));\n          border-left-color: mc('blue', '500');\n          cursor: pointer;\n\n          @at-root .is-rtl & {\n            border-left-color: mc('grey', '200');\n            border-right-width: mc('blue', '500');\n          }\n        }\n\n        &::before {\n          content: '';\n          display: none;\n        }\n\n        > a {\n          display: block;\n          text-decoration: none;\n          margin: -1rem;\n          padding: 1rem;\n\n          > em {\n            font-weight: 400;\n            font-style: normal;\n            color: mc('grey', '700');\n            display: inline-block;\n            padding-left: .5rem;\n            border-left: 1px solid mc('grey', '300');\n            margin-left: .5rem;\n\n            &.is-block {\n              display: block;\n              padding-left: 0;\n              margin-left: 0;\n              border-left: none;\n            }\n          }\n        }\n\n        > em {\n          font-weight: 400;\n          font-style: normal;\n        }\n\n        @at-root .theme--dark & {\n          background-color: mc('grey', '50');\n          background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 5%), mc('grey', '900'));\n          border-right: 1px solid mc('grey', '900');\n          border-bottom: 1px solid mc('grey', '900');\n          border-left: 5px solid mc('grey', '700');\n          box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.1);\n\n          @at-root .theme--dark.is-rtl & {\n            border-left-width: 1px;\n            border-right-width: 5px;\n          }\n\n          &:hover {\n            background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 2%), darken(mc('grey', '900'), 3%));\n            border-left-color: mc('indigo', '300');\n            cursor: pointer;\n\n            @at-root .theme--dark.is-rtl & {\n              border-left-color: mc('grey', '900');\n              border-right-width: mc('indigo', '300');\n            }\n          }\n        }\n      }\n    }\n\n    &.grid-list {\n      margin: 1rem 0 0 0;\n      background-color: #FFF;\n      border: 1px solid mc('grey', '200');\n      padding: 1px;\n      display: inline-block;\n      list-style-type: none;\n\n      @at-root .theme--dark & {\n        background-color: #000;\n        border: 1px solid mc('grey', '800');\n      }\n\n      li {\n        background-color: mc('grey', '50');\n        padding: .6rem 1rem;\n        display: block;\n\n        &:nth-child(odd) {\n          background-color: mc('grey', '100');\n        }\n\n        & + li {\n          margin-top: 0;\n        }\n\n        &::before {\n          content: '';\n          display: none;\n        }\n\n        @at-root .theme--dark & {\n          background-color: mc('grey', '900');\n\n          &:nth-child(odd) {\n            background-color: darken(mc('grey', '900'), 5%);\n          }\n        }\n      }\n    }\n  }\n\n  ul:not(.tabset-tabs):not(.contains-task-list) {\n    list-style: none;\n    > li::before {\n      position: absolute;\n      left: -1.1rem;\n      content: '\\25b8';\n      color: mc('grey', '600');\n      width: 1.35rem;\n\n      @at-root .is-rtl & {\n        right: -1.1rem;\n        content: '\\25C3';\n      }\n    }\n  }\n  ol, ul:not(.tabset-tabs) {\n    > li {\n      position: relative;\n      > p {\n        display:inline-block;\n        vertical-align:top;\n        padding-top:0;\n\n        &:first-child {\n          width: 100%;\n        }\n      }\n    }\n  }\n\n  dl {\n    dt {\n      margin-top: 0.3em;\n      margin-bottom: 0.3em;\n      font-weight: bold;\n    }\n\n    dd {\n      margin-left: 1.125em;\n      margin-bottom: 0.75em;\n\n      > p {\n        padding: 0;\n      }\n    }\n  }\n\n  // ---------------------------------\n  // CODE\n  // ---------------------------------\n\n  code {\n    background-color: mc('indigo', '50');\n    padding: 0 5px;\n    color: mc('indigo', '800');\n    font-family: 'Roboto Mono', monospace;\n    font-weight: normal;\n    font-size: 1rem;\n    box-shadow: none;\n\n    &::before, &::after {\n      display: none;\n    }\n\n    @at-root .theme--dark & {\n      background-color: darken(mc('grey', '900'), 5%);\n      color: mc('indigo', '100');\n    }\n  }\n\n  .prismjs{\n    border: none;\n    border-radius: 5px;\n    box-shadow: initial;\n    background-color: mc('grey', '900');\n    padding: 1rem 1rem 1rem 3rem;\n    margin: 1rem 0;\n\n    @at-root .theme--dark & {\n      background-color: darken(mc('grey', '900'), 5%);\n    }\n\n    > code {\n      background-color: transparent;\n      padding: 0;\n      color: #FFF;\n      box-shadow: initial;\n      display: block;\n      font-size: .85rem;\n      font-family: 'Roboto Mono', monospace;\n\n      &:after, &:before {\n        content: initial;\n        letter-spacing: initial;\n      }\n    }\n  }\n\n  .diagram {\n    margin-top: 1rem;\n    overflow: auto;\n\n    svg {\n      color-scheme: light !important;\n\n      &:first-child {\n        direction: ltr;\n      }\n\n      @at-root .theme--dark & {\n        color-scheme: dark !important;\n      }\n    }\n  }\n\n  // ---------------------------------\n  // TASK LISTS\n  // ---------------------------------\n\n  .contains-task-list {\n    padding-left: 0;\n  }\n\n  .task-list-item {\n    position: relative;\n    list-style-type: none;\n\n    &-checkbox[disabled] {\n      width: 1.1rem;\n      height: 1.1rem;\n      top: 2px;\n      position: relative;\n      margin-right: 2px;\n\n      &::after {\n        position: absolute;\n        left: 0;\n        top: 0;\n        content: ' ';\n        display: block;\n        width: 1.1rem;\n        height: 1.1rem;\n        background-color: #FFF;\n        border: 1px solid mc('grey', '400');\n        border-radius: 2px;\n        font-weight: bold;\n        font-size: .8rem;\n        line-height: 1rem;\n        text-align: center;\n\n        @at-root .theme--dark & {\n          background-color: mc('grey', '900');\n          border-color: mc('grey', '700');\n        }\n      }\n\n      &[checked]::after  {\n        content: '✓';\n      }\n    }\n\n    .contains-task-list {\n      padding: .5rem 0 0 1.5rem;\n    }\n  }\n\n  // ---------------------------------\n  // TABLES\n  // ---------------------------------\n\n  table {\n    margin: .5rem 0;\n    border-spacing: 0;\n    border-radius: 5px;\n    border: 1px solid mc('grey', '300');\n\n    @at-root .theme--dark & {\n      border-color: mc('grey', '600');\n    }\n\n    &.dense {\n      td, th {\n        font-size: .85rem;\n        padding: .5rem;\n      }\n    }\n\n    th {\n      padding: .75rem;\n      border-bottom: 2px solid mc('grey', '500');\n      color: mc('grey', '600');\n      background-color: mc('grey', '100');\n\n      @at-root .theme--dark & {\n        background-color: darken(mc('grey', '900'), 8%);\n        border-bottom-color: mc('grey', '600');\n        color: mc('grey', '500');\n      }\n\n      &:first-child {\n        border-top-left-radius: 7px;\n      }\n      &:last-child {\n        border-top-right-radius: 7px;\n      }\n    }\n\n    td {\n      padding: .75rem;\n    }\n\n    tr {\n      td {\n        border-bottom: 1px solid mc('grey', '300');\n        border-right: 1px solid mc('grey', '100');\n\n        @at-root .theme--dark & {\n          border-bottom-color: mc('grey', '700');\n          border-right-color: mc('grey', '800');\n        }\n\n        &:nth-child(even) {\n          background-color: mc('grey', '50');\n\n          @at-root .theme--dark & {\n            background-color: darken(mc('grey', '900'), 4%);\n          }\n        }\n\n        &:last-child {\n          border-right: none;\n        }\n      }\n\n      &:nth-child(even) {\n        td {\n          background-color: mc('grey', '50');\n\n          @at-root .theme--dark & {\n            background-color: darken(mc('grey', '800'), 8%);\n          }\n\n          &:nth-child(even) {\n            background-color: mc('grey', '100');\n\n            @at-root .theme--dark & {\n              background-color: darken(mc('grey', '800'), 10%);\n            }\n          }\n        }\n      }\n\n      &:last-child {\n        td {\n          border-bottom: none;\n\n          &:first-child {\n            border-bottom-left-radius: 7px;\n          }\n          &:last-child {\n            border-bottom-right-radius: 7px;\n          }\n        }\n      }\n    }\n  }\n\n  figure.table {\n    margin: 0;\n\n    > table {\n      background-color: #FFF;\n      margin: 0;\n      border-collapse: collapse;\n      box-shadow: 0 0 5px 0 rgba(0, 0, 0, .07);\n\n      @at-root .theme--dark & {\n        background-color: darken(mc('grey', '900'), 3%);\n      }\n\n      td, th {\n        border: 1px solid mc('blue-grey', '100');\n        box-shadow: inset -1px -1px 0 0 #FFF, inset 1px 0 0 #FFF;\n        padding: .5rem .75rem;\n        border-radius: 0 !important;\n\n        @at-root .theme--dark & {\n          border-color: mc('grey', '700');\n          box-shadow: inset -1px -1px 0 0 rgba(0,0,0, .5);\n        }\n      }\n\n      th {\n        background-color: lighten(mc('blue-grey', '50'), 1%);\n        font-weight: 700;\n        color: mc('blue-grey', '700');\n\n        @at-root .theme--dark & {\n          background-color: mc('grey', '800');\n          color: mc('grey', '400');\n        }\n      }\n\n      thead th {\n        border-bottom: 2px solid mc('blue-grey', '100');\n\n        @at-root .theme--dark & {\n          border-bottom: none;\n        }\n      }\n\n      tbody th {\n        background-color: lighten(mc('blue-grey', '50'), 4%);\n\n        @at-root .theme--dark & {\n          background-color: darken(mc('grey', '800'), 8%);\n        }\n      }\n    }\n  }\n\n  // -> Add horizontal scrollbar when table is too wide\n  .table-container {\n    width: 100%;\n    overflow-x: auto;\n  }\n\n  // ---------------------------------\n  // IMAGES\n  // ---------------------------------\n\n  img {\n    max-width: 100%;\n\n    &.align-left {\n      float: left;\n      margin: 0 1rem 1rem 0;\n    }\n    &.align-right {\n      float: right;\n      margin: 0 0 1rem 1rem;\n      z-index: 1;\n      position: relative;\n    }\n    &.align-center {\n      display: block;\n      max-width: 100%;\n      margin: auto;\n    }\n    &.align-abstopright {\n      position: absolute;\n      top: -90px;\n      right: 1rem;\n      height: calc(90px - 32px);\n      width: auto;\n\n      @at-root .is-rtl & {\n        left: 1rem;\n        right: initial;\n      }\n    }\n    &.decor-shadow {\n      box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);\n    }\n    &.decor-outline {\n      border: 1px solid mc('grey', '400');\n    }\n    &.uml-diagram {\n      margin: 1rem 0;\n    }\n  }\n\n  figure.image {\n    margin: 1rem 0 0 0;\n\n    img {\n      margin: 0 auto;\n    }\n    figcaption {\n      padding: 4px 1rem;\n      text-align: center;\n      font-size: 12px;\n      color: mc('grey', '700');\n      background-color: mc('grey', '100');\n\n      @at-root .theme--dark & {\n        color: mc('grey', '400');\n        background-color: mc('grey', '800');\n      }\n    }\n  }\n\n  figure.image-style-align-right {\n    float: right;\n  }\n\n  figure.image-style-align-left {\n    float: left;\n  }\n\n  // ---------------------------------\n  // DETAILS\n  // ---------------------------------\n\n  details {\n    background-color: mc('grey', '50');\n    margin: 1rem 2rem;\n    border: 1px solid mc('grey', '300');\n    border-radius: 7px;\n\n    > p {\n      padding-left: 0;\n    }\n\n    > summary {\n      border-radius: 7px;\n      background-color: mc('grey', '50');\n      cursor: pointer;\n      display: list-item;\n      align-items: center;\n      padding: 0.4rem 1rem;\n      transition: background-color .4s ease;\n\n      &:focus {\n        outline: none;\n        background-color: mc('grey', '100');\n      }\n\n      > h1, h2, h3, h4, h5, h6 {\n        width: 95%;\n        display: inline-block;\n\n        &:first-child {\n          margin-top: 0;\n        }\n\n        &:only-child::after {\n          display: none;\n        }\n      }\n    }\n\n    &[open] {\n      padding: 1rem;\n\n      > summary {\n        background-color: mc('grey', '100');\n        border-bottom: 1px solid mc('grey', '300');\n        border-bottom-left-radius: 0;\n        border-bottom-right-radius: 0;\n        margin: -1rem -1rem 1rem -1rem;\n      }\n    }\n\n    @at-root .theme--dark & {\n      background-color: mc('grey', '900');\n      border-color: mc('grey', '700');\n\n      > summary {\n        background-color: mc('grey', '900');\n        border-color: mc('grey', '700');\n      }\n\n      &[open] > summary {\n        background-color: lighten(mc('grey', '900'), 5%);\n      }\n    }\n\n  }\n\n  // ---------------------------------\n  // HIGHLIGHTING\n  // ---------------------------------\n\n  mark {\n    &.pen-red {\n      color: mc('red', '500');\n      background-color: initial;\n    }\n    &.pen-green {\n      color: mc('green', '500');\n      background-color: initial;\n    }\n    &.marker-blue {\n      background-color: mc('blue', '300');\n    }\n    &.marker-yellow {\n      background-color: mc('yellow', '300');\n    }\n    &.marker-pink {\n      background-color: mc('pink', '300');\n    }\n    &.marker-green {\n      background-color: mc('green', '300');\n    }\n  }\n\n  .mention {\n    background-color: rgba(153, 0, 48, .1);\n    color: #990030;\n\n    @at-root .theme--dark & {\n      color: mc('pink', '500');\n    }\n  }\n\n}\n\n// ---------------------------------\n// COMMENTS\n// ---------------------------------\n\n.comments {\n  &-container {\n    border-radius: 7px;\n  }\n\n  &-header {\n    color: #FFF;\n    padding: 8px 20px;\n    font-size: 16px;\n    font-weight: 500;\n    background-color: mc('blue-grey', '500');\n    border-radius: 7px 7px 0 0;\n\n    @at-root .theme--dark & {\n      background-color: lighten(mc('blue-grey', '900'), 5%);\n    }\n  }\n\n  &-main {\n    background-color: mc('blue-grey', '50');\n    border-radius: 0 0 7px 7px;\n    padding: 20px;\n\n    @at-root .theme--dark & {\n      background-color: darken(mc('grey', '900'), 5%);\n    }\n  }\n}\n\n// ---------------\n// RTL FIXES\n// Vuetify GH Issue: https://github.com/vuetifyjs/vuetify/issues/6317\n// ---------------\n\n.is-rtl {\n  .page-col-content.is-page-header {\n    @each $size, $width in $grid-breakpoints {\n      @media (min-width: $width) {\n        @for $n from 0 through 12 {\n          &.offset-#{$size}-#{$n} {\n            margin-left: 0;\n            margin-right: ($n / 12 * 100) * 1%;\n          }\n        }\n      }\n    }\n  }\n}\n\n// ---------------\n// PRINT OVERRIDES\n// ---------------\n\n@media print {\n  .nav-header,\n  .v-navigation-drawer,\n  .v-btn--fab,\n  .page-col-sd,\n  .v-tooltip__content\n  {\n    display: none !important;\n  }\n\n  .layout {\n    display: block !important;\n  }\n\n  .page-col-content {\n    flex-basis: 100% !important;\n    flex-grow: 1 !important;\n    max-width: 100% !important;\n    margin-left: 0 !important;\n\n    > .v-toolbar {\n      border: 1px solid mc('grey', '300') !important;\n      border-radius: 7px !important;\n\n      & + .v-divider {\n        display: none !important;\n      }\n    }\n  }\n\n  .v-main {\n    padding: 0 !important;\n    font-size: 14px;\n    background-color: #FFF;\n  }\n\n  .v-main .contents {\n    color: #000;\n    background-color: #FFF;\n\n    @at-root .theme--dark & {\n      color: #000;\n    }\n\n    .prismjs{\n      box-shadow: none;\n      background-color: #FFF;\n\n      @at-root .theme--dark & {\n        background-color: #FFF;\n      }\n\n      > code {\n        color: #000;\n        box-shadow: none;\n        text-shadow: none;\n        white-space: pre-wrap !important;\n        overflow-wrap: break-word !important;\n      }\n    }\n  }\n\n  .comments-container {\n    display: none;\n  }\n\n  .page-edit-shortcuts {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "client/themes/default/theme.yml",
    "content": "name: Default\nauthor: requarks.io\nsite: https://wiki.requarks.io/\nversion: 1.0.0\nrequirements:\n  minimum: '>= 2.0.0'\n  maximum: '< 3.0.0'\nprops:\n  sdPosition:\n    type: String\n    default: 'left'\n    title: Table of Contents Position\n    hint: Should the content sidebar be shown on the left or right.\n    enum:\n      - 'hidden'\n      - 'left'\n      - 'right'\n    order: 1\n    icon: mdi-border-vertical\n  showTOC:\n    type: Boolean\n    default: true\n    title: Display the Table of Contents\n    order: 2\n  showTags:\n    type: Boolean\n    default: true\n    title: Display the Page Tags\n    order: 3\n  showTags:\n    type: Boolean\n    default: true\n    title: Display the Page Author and Date\n    order: 4\n  showTags:\n    type: Boolean\n    default: true\n    title: Display the Page Rating\n    order: 5\n  showSocialBar:\n    type: Boolean\n    default: true\n    title: Display the Social Links Bar\n    order: 6\n  showEditSpeedDial:\n    type: Boolean\n    default: true\n    title: Display the Edit Speed Dial\n    hint: Shown in the lower right corner of the page.\n    order: 7\n\n"
  },
  {
    "path": "config.sample.yml",
    "content": "#######################################################################\n# Wiki.js - CONFIGURATION                                             #\n#######################################################################\n# Full documentation + examples:\n# https://docs.requarks.io/install\n\n# ---------------------------------------------------------------------\n# Port the server should listen to\n# ---------------------------------------------------------------------\n\nport: 3000\n\n# ---------------------------------------------------------------------\n# Database\n# ---------------------------------------------------------------------\n# Supported Database Engines:\n# - postgres = PostgreSQL 9.5 or later\n# - mysql = MySQL 8.0 or later (5.7.8 partially supported, refer to docs)\n# - mariadb = MariaDB 10.2.7 or later\n# - mssql = MS SQL Server 2012 or later\n# - sqlite = SQLite 3.9 or later\n\ndb:\n  type: postgres\n\n  # PostgreSQL / MySQL / MariaDB / MS SQL Server only:\n  host: localhost\n  port: 5432\n  user: wikijs\n  pass: wikijsrocks\n  db: wiki\n  ssl: false\n\n  # Optional - PostgreSQL / MySQL / MariaDB only:\n  # -> Uncomment lines you need below and set `auto` to false\n  # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options\n  sslOptions:\n    auto: true\n    # rejectUnauthorized: false\n    # ca: path/to/ca.crt\n    # cert: path/to/cert.crt\n    # key: path/to/key.pem\n    # pfx: path/to/cert.pfx\n    # passphrase: xyz123\n\n  # Optional - PostgreSQL only:\n  schema: public\n\n  # SQLite only:\n  storage: path/to/database.sqlite\n\n#######################################################################\n# ADVANCED OPTIONS                                                    #\n#######################################################################\n# Do not change unless you know what you are doing!\n\n# ---------------------------------------------------------------------\n# SSL/TLS Settings\n# ---------------------------------------------------------------------\n# Consider using a reverse proxy (e.g. nginx) if you require more\n# advanced options than those provided below.\n\nssl:\n  enabled: false\n  port: 3443\n\n  # Provider to use, possible values: custom, letsencrypt\n  provider: custom\n\n  # ++++++ For custom only ++++++\n  # Certificate format, either 'pem' or 'pfx':\n  format: pem\n  # Using PEM format:\n  key: path/to/key.pem\n  cert: path/to/cert.pem\n  # Using PFX format:\n  pfx: path/to/cert.pfx\n  # Passphrase when using encrypted PEM / PFX keys (default: null):\n  passphrase: null\n  # Diffie Hellman parameters, with key length being greater or equal\n  # to 1024 bits (default: null):\n  dhparam: null\n\n  # ++++++ For letsencrypt only ++++++\n  domain: wiki.yourdomain.com\n  subscriberEmail: admin@example.com\n\n# ---------------------------------------------------------------------\n# Database Pool Options\n# ---------------------------------------------------------------------\n# Refer to https://github.com/vincit/tarn.js for all possible options\n\npool:\n  # min: 2\n  # max: 10\n\n# ---------------------------------------------------------------------\n# IP address the server should listen to\n# ---------------------------------------------------------------------\n# Leave 0.0.0.0 for all interfaces\n\nbindIP: 0.0.0.0\n\n# ---------------------------------------------------------------------\n# Log Level\n# ---------------------------------------------------------------------\n# Possible values: error, warn, info (default), verbose, debug, silly\n\nlogLevel: info\n\n# ---------------------------------------------------------------------\n# Log Format\n# ---------------------------------------------------------------------\n# Output format for logging, possible values: default, json\n\nlogFormat: default\n\n# ---------------------------------------------------------------------\n# Offline Mode\n# ---------------------------------------------------------------------\n# If your server cannot access the internet. Set to true and manually\n# download the offline files for sideloading.\n\noffline: false\n\n# ---------------------------------------------------------------------\n# High-Availability\n# ---------------------------------------------------------------------\n# Set to true if you have multiple concurrent instances running off the\n# same DB (e.g. Kubernetes pods / load balanced instances). Leave false\n# otherwise. You MUST be using PostgreSQL to use this feature.\n\nha: false\n\n# ---------------------------------------------------------------------\n# Data Path\n# ---------------------------------------------------------------------\n# Writeable data path used for cache and temporary user uploads.\ndataPath: ./data\n\n# ---------------------------------------------------------------------\n# Body Parser Limit\n# ---------------------------------------------------------------------\n# Maximum size of API requests body that can be parsed. Does not affect\n# file uploads.\n\nbodyParserLimit: 5mb\n"
  },
  {
    "path": "cypress.json",
    "content": "{\n  \"baseUrl\": \"http://localhost:3000\",\n  \"projectId\": \"r7qxah\",\n  \"fixturesFolder\": false,\n  \"integrationFolder\": \"dev/cypress/integration\",\n  \"pluginsFile\": \"dev/cypress/plugins/index.js\",\n  \"screenshotsFolder\": \"dev/cypress/screenshots\",\n  \"supportFile\": \"dev/cypress/support/index.js\",\n  \"videosFolder\": \"dev/cypress/videos\",\n  \"numTestsKeptInMemory\": 1\n}\n"
  },
  {
    "path": "dev/build/Dockerfile",
    "content": "# ====================\n# --- Build Assets ---\n# ====================\nFROM node:24-alpine AS assets\n\nRUN apk add yarn g++ make cmake python3 --no-cache\n\nWORKDIR /wiki\n\nCOPY ./client ./client\nCOPY ./dev ./dev\nCOPY ./patches ./patches\nCOPY ./package.json ./package.json\nCOPY ./.babelrc ./.babelrc\nCOPY ./.eslintignore ./.eslintignore\nCOPY ./.eslintrc.yml ./.eslintrc.yml\n\nRUN yarn cache clean\nRUN yarn --frozen-lockfile --non-interactive\nRUN yarn build\nRUN rm -rf /wiki/node_modules\nRUN yarn --production --frozen-lockfile --non-interactive\nRUN yarn patch-package\n\n# ===============\n# --- Release ---\n# ===============\nFROM node:24-alpine\nLABEL maintainer=\"requarks.io\"\n\nRUN apk add bash curl git openssh gnupg sqlite --no-cache && \\\n    mkdir -p /wiki && \\\n    mkdir -p /logs && \\\n    mkdir -p /wiki/data/content && \\\n    chown -R node:node /wiki /logs\n\nWORKDIR /wiki\n\nCOPY --chown=node:node --from=assets /wiki/assets ./assets\nCOPY --chown=node:node --from=assets /wiki/node_modules ./node_modules\nCOPY --chown=node:node ./server ./server\nCOPY --chown=node:node --from=assets /wiki/server/views ./server/views\nCOPY --chown=node:node ./dev/build/config.yml ./config.yml\nCOPY --chown=node:node ./package.json ./package.json\nCOPY --chown=node:node ./LICENSE ./LICENSE\n\nUSER node\n\nVOLUME [\"/wiki/data/content\"]\n\nEXPOSE 3000\nEXPOSE 3443\n\n# HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://localhost:3000/healthz\n\nCMD [\"node\", \"--no-deprecation\", \"server\"]\n"
  },
  {
    "path": "dev/build/config.yml",
    "content": "port: 3000\nbindIP: 0.0.0.0\ndb:\n  type: $(DB_TYPE)\n  host: '$(DB_HOST)'\n  port: $(DB_PORT)\n  user: '$(DB_USER)'\n  pass: '$(DB_PASS)'\n  db: $(DB_NAME)\n  storage: $(DB_FILEPATH)\n  ssl: $(DB_SSL)\nssl:\n  enabled: $(SSL_ACTIVE)\n  port: 3443\n  provider: letsencrypt\n  domain: $(LETSENCRYPT_DOMAIN)\n  subscriberEmail: $(LETSENCRYPT_EMAIL)\nlogLevel: $(LOG_LEVEL:info)\nlogFormat: $(LOG_FORMAT:default)\nha: $(HA_ACTIVE)\n"
  },
  {
    "path": "dev/build-arm/Dockerfile",
    "content": "# =========================\n# --- BUILD NPM MODULES ---\n# =========================\nFROM node:20-alpine AS build\n\nRUN apk add yarn g++ make cmake python3 --no-cache\n\nWORKDIR /wiki\n\nCOPY ./package.json ./package.json\nCOPY ./patches ./patches\n\nRUN yarn --production --frozen-lockfile --non-interactive --network-timeout 100000\nRUN yarn patch-package\n\n# ===============\n# --- Release ---\n# ===============\nFROM node:20-alpine\nLABEL maintainer=\"requarks.io\"\n\nRUN apk add bash curl git openssh gnupg sqlite --no-cache && \\\n    mkdir -p /wiki && \\\n    mkdir -p /logs && \\\n    mkdir -p /wiki/data/content && \\\n    chown -R node:node /wiki /logs\n\nWORKDIR /wiki\n\nCOPY --chown=node:node ./build/assets ./assets\nCOPY --chown=node:node --from=build /wiki/node_modules ./node_modules\nCOPY --chown=node:node ./server ./server\nCOPY --chown=node:node ./build/server/views ./server/views\nCOPY --chown=node:node ./dev/build/config.yml ./config.yml\nCOPY --chown=node:node ./build/package.json ./package.json\nCOPY --chown=node:node ./LICENSE ./LICENSE\n\nUSER node\n\nVOLUME [\"/wiki/data/content\"]\n\nEXPOSE 3000\nEXPOSE 3443\n\nCMD [\"node\", \"server\"]\n"
  },
  {
    "path": "dev/containers/Dockerfile",
    "content": "# -- DEV DOCKERFILE --\n# -- DO NOT USE IN PRODUCTION! --\n\nFROM node:24\nLABEL maintainer \"requarks.io\"\n\nRUN apt-get update && \\\n    apt-get install -y bash curl git python3 make g++ nano openssh-server gnupg && \\\n    mkdir -p /wiki\n\nWORKDIR /wiki\n\nENV dockerdev 1\nENV DEVDB postgres\n\nEXPOSE 3000\n\nCMD [\"tail\", \"-f\", \"/dev/null\"]\n"
  },
  {
    "path": "dev/containers/config.yml",
    "content": "port: 3000\nbindIP: 0.0.0.0\ndb:\n  type: postgres\n  host: db\n  port: 5432\n  user: wikijs\n  pass: wikijsrocks\n  db: wiki\nlogLevel: info\n"
  },
  {
    "path": "dev/containers/docker-compose.yml",
    "content": "# -- DEV DOCKER-COMPOSE --\n# -- DO NOT USE IN PRODUCTION! --\n\nversion: \"3\"\nservices:\n  db:\n    container_name: wiki-db\n    image: postgres:17-alpine\n    environment:\n      POSTGRES_DB: wiki\n      POSTGRES_PASSWORD: wikijsrocks\n      POSTGRES_USER: wikijs\n    logging:\n      driver: \"none\"\n    volumes:\n      - db-data:/var/lib/postgresql/data\n    ports:\n      - \"15432:5432\"\n\n  adminer:\n    container_name: wiki-adminer\n    image: adminer:latest\n    logging:\n      driver: \"none\"\n    ports:\n      - \"3001:8080\"\n\n  # solr:\n  #   container_name: wiki-solr\n  #   image: solr:7-alpine\n  #   logging:\n  #     driver: \"none\"\n  #   ports:\n  #     - \"8983:8983\"\n  #   volumes:\n  #     - solr-data:/opt/solr/server/solr/mycores\n  #   entrypoint:\n  #     - docker-entrypoint.sh\n  #     - solr-precreate\n  #     - wiki\n\n  wiki:\n    container_name: wiki-app\n    build:\n      context: ../..\n      dockerfile: dev/containers/Dockerfile\n    depends_on:\n      - db\n    ports:\n      - \"3000:3000\"\n    volumes:\n      - ../..:/wiki\n      - /wiki/node_modules\n      - /wiki/.git\n\n\nvolumes:\n  db-data:\n  # solr-data:\n"
  },
  {
    "path": "dev/cypress/ci-setup.sh",
    "content": "case $MATRIXENV in\npostgres)\n  echo \"Using PostgreSQL...\"\n  docker run -d -p 5432:5432 --name db --network=\"host\" -e \"POSTGRES_PASSWORD=Password123!\" -e \"POSTGRES_USER=wiki\" -e \"POSTGRES_DB=wiki\" postgres:11\n  while ! docker exec db psql -U wiki -d wiki -c \"SELECT 1\" &> /dev/null ; do\n    echo \"Waiting for database connection...\"\n    sleep 2\n  done\n  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\n  ;;\nmysql)\n  echo \"Using MySQL...\"\n  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\n  while ! docker exec db mysql --user=root --password=Password123! -e \"SELECT 1\" &> /dev/null ; do\n    echo \"Waiting for database connection...\"\n    sleep 2\n  done\n  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\n  ;;\nmariadb)\n  echo \"Using MariaDB...\"\n  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\n  while ! docker exec db mysql --user=root --password=Password123! -e \"SELECT 1\" &> /dev/null ; do\n    echo \"Waiting for database connection...\"\n    sleep 2\n  done\n  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\n  ;;\nmssql)\n  echo \"Using MS SQL Server...\"\n  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\n  while ! docker exec db /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P \"Password123!\" -Q 'CREATE DATABASE wiki' &> /dev/null ; do\n    echo \"Waiting for database connection...\"\n    sleep 2\n  done\n  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\n  ;;\nsqlite)\n  echo \"Using SQLite...\"\n  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\n  ;;\n*)\n  echo \"Invalid DB Type!\"\n  ;;\nesac\n"
  },
  {
    "path": "dev/cypress/integration/setup.spec.js",
    "content": "/// <reference types=\"Cypress\" />\n\ndescribe('Setup', () => {\n  it('Load the setup page', () => {\n    cy.visit('/')\n    cy.contains('You are about to install Wiki.js').should('exist')\n  })\n  it('Enter administrator email address', () => {\n    cy.get('.v-input').contains('Administrator Email').next('input').click().type('test@example.com')\n  })\n  it('Enter a password', () => {\n    cy.get('.v-input').contains('Password').next('input').click().type('12345678')\n    cy.get('.v-input').contains('Confirm Password').next('input').click().type('12345678')\n  })\n  it('Enter a Site URL', () => {\n    cy.get('.v-input').contains('Site URL').next('input').click().clear().type('http://localhost:3000')\n  })\n  it('Disable Telemetry', () => {\n    cy.contains('Telemetry').next('.v-input').click()\n  })\n  it('Press Install', () => {\n    cy.get('.v-card__actions').find('button').click()\n  })\n  it('Wait for install success', () => {\n    cy.contains('Installation complete!', {timeout: 30000}).should('exist')\n  })\n  // -> Disabled because of origin change errors during CI tests\n  //\n  // it('Redirect to login page', () => {\n  //   cy.location('pathname', {timeout: 10000}).should('include', '/login')\n  // })\n})\n"
  },
  {
    "path": "dev/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\n/**\n * @type {Cypress.PluginConfig}\n */\nmodule.exports = (on, config) => {\n  // `on` is used to hook into various events Cypress emits\n  // `config` is the resolved Cypress config\n}\n"
  },
  {
    "path": "dev/cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n"
  },
  {
    "path": "dev/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands'\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "dev/examples/docker-compose.yml",
    "content": "version: \"3\"\nservices:\n\n  db:\n    image: postgres:15-alpine\n    environment:\n      POSTGRES_DB: wiki\n      POSTGRES_PASSWORD: wikijsrocks\n      POSTGRES_USER: wikijs\n    logging:\n      driver: \"none\"\n    restart: unless-stopped\n    volumes:\n      - db-data:/var/lib/postgresql/data\n\n  wiki:\n    image: requarks/wiki:2\n    depends_on:\n      - db\n    environment:\n      DB_TYPE: postgres\n      DB_HOST: db\n      DB_PORT: 5432\n      DB_USER: wikijs\n      DB_PASS: wikijsrocks\n      DB_NAME: wiki\n    restart: unless-stopped\n    ports:\n      - \"80:3000\"\n      - \"443:3443\"\n\nvolumes:\n  db-data:\n"
  },
  {
    "path": "dev/helm/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "dev/helm/Chart.yaml",
    "content": "apiVersion: v2\nname: wiki\nversion: '3.0.0'\nappVersion: '2'\ndescription: The most powerful and extensible open source Wiki software.\nkeywords:\n  - wiki\n  - documentation\n  - knowledge base\n  - docs\n  - reference\n  - editor\ntype: application\nhome: https://js.wiki\nicon: https://cdn.js.wiki/images/wikijs-butterfly.svg\nsources:\n  - https://github.com/requarks/wiki\n"
  },
  {
    "path": "dev/helm/README.md",
    "content": "<div align=\"center\">\n\n<img src=\"https://static.requarks.io/logo/wikijs-full.svg\" alt=\"Wiki.js\" width=\"600\" />\n\n[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases)\n[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE)\n[![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat&logo=javascript&logoColor=white)](http://standardjs.com/)\n[![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases)\n[![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/)  \n[![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml)\n[![Huntr](https://img.shields.io/badge/security%20bounty-disclose-brightgreen.svg?style=flat&logo=cachet&logoColor=white)](https://huntr.dev/bounties/disclose)\n[![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)\n[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)  \n[![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack)\n[![Twitter Follow](https://img.shields.io/badge/follow-%40requarks-blue.svg?style=flat&logo=twitter)](https://twitter.com/requarks)\n[![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/)\n[![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp)](https://blog.js.wiki/subscribe)\n\n##### A modern, lightweight and powerful wiki app built on NodeJS\n\n</div>\n\n- **[Official Website](https://wiki.js.org/)**\n- **[Documentation](https://docs.requarks.io/)**\n\n<h2 align=\"center\">Donate</h2>\n\n<div align=\"center\">\n\nWiki.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`).\n  \n  [![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship)\n  [![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks)\n  [![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)\n  [![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)  \n  [![Donate via Ethereum](https://img.shields.io/badge/donate-ethereum-999.svg?style=popout&logo=ethereum&logoColor=CCC)](https://etherscan.io/address/0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5)\n  [![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)\n  [![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)\n\n</div>\n\n## Introduction\n\nThis chart bootstraps a Wiki.js deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.\n\nIt also optionally deploys PostgreSQL as the database using the official PostgreSQL image from Docker Hub, but you are free to bring your own database.\n\n## Prerequisites\n\n- PV provisioner support in the underlying infrastructure (with persistence storage enabled) if you want data persistance\n\n## Adding the Wiki.js Helm Repository\n\n```console\n$ helm repo add requarks https://charts.js.wiki\n```\n\n## Installing the Chart\n\nTo install the chart with the release name `my-release` run the following:\n\n### Using Helm 3/4:\n```console\n$ helm install my-release requarks/wiki\n```\n### Using Helm 2:\n```console\n$ helm install --name my-release requarks/wiki\n```\n\nThe 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.\n\n> **Tip**: List all releases using `helm list`\n\n## Uninstalling the Chart\n\nTo uninstall/delete the `my-release` deployment:\n\n```console\n$ helm delete my-release\n```\n\nThe command removes all the Kubernetes components associated with the chart and deletes the release.\n\n> **Warning**: Persistant Volume Claims for the database are not deleted automatically. They need to be manually deleted\n\n```console\n$ kubectl delete pvc/data-wiki-postgresql-0\n```\n\n## Configuration\n\nThe following table lists the configurable parameters of the Wiki.js chart and their default values.\n\n| Parameter                            | Description                                 | Default                                                    |\n| -------------------------------      | -------------------------------             | ---------------------------------------------------------- |\n| `image.repository`                   | Wiki.js image                                | `requarks/wiki`                                           |\n| `image.tag`                          | Wiki.js image tag                            | `2`                                                      |\n| `imagePullPolicy`                    | Image pull policy                           | `IfNotPresent`                                             |\n| `replicacount`                       | Number of Wiki.js pods to run                   | `1`                                                        |\n| `revisionHistoryLimit`               | Total number of revision history points                   | `10`                                        |\n| `resources.limits`               | Wiki.js service resource limits                         | `nil`                               |\n| `resources.requests`             | Wiki.js service resource requests                       | `nil`                               |\n| `nodeSelector`                   | Node labels for the Wiki.js pod assignment          | `{}`                                                       |\n| `affinity`                       | Affinity settings for the Wiki.js pod assignment    | `{}`                                                       |\n| `schedulerName`                  | Name of an alternate scheduler for the Wiki.js pod  | `nil`                                                      |\n| `tolerations`                    | Toleration labels for the Wiki.js pod assignment    | `[]`                                                       |\n| `volumeMounts`                   | Volume mounts for the Wiki.js container              | `[]`                                                       |\n| `volumes`                        | Volumes for the Wiki.js pod                          | `[]`                                                       |\n| `ingress.enabled`                    | Enable ingress controller resource          | `false`                                                    |\n| `ingress.className`                  | Ingress class name                          | `\"\"`                                                       |\n| `ingress.annotations`                | Ingress annotations                         | `{}`                                                       |\n| `ingress.hosts`                      | List of ingress rules                        | `[{\"host\": \"wiki.local\", \"paths\": [\"/\"]}]`                |\n| `ingress.tls`                        | Ingress TLS configuration                   | `[]`                                                       |\n| `sideload.enabled`                   | Enable sideloading of locale files from git | `false`                                                    |\n| `sideload.repoURL`                   | Git repository URL containing locale files  | `https://github.com/Requarks/wiki-localization`            |\n| `sideload.env`                       | Environment variables for the sideload container | `{}`                                                      |\n| `sideload.securityContext`           | Security context for the sideload container     | `nil`                                                      |\n| `sideload.resources.limits`          | Resource limits for the sideload container      | `nil`                                                      |\n| `sideload.resources.requests`        | Resource requests for the sideload container    | `nil`                                                      |\n| `nodeExtraCaCerts`                   | Trusted certificates path                   | `nil`                                                      |\n| `externalPostgresql.databaseURL`     | External postgres connection string         | `nil`                                                  |\n| `postgresql.enabled`                 | Deploy postgres server (see below)          | `true`                                                     |\n| `postgresql.postgresqlDatabase`        | Postgres database name                      | `wiki`                                                   |\n| `postgresql.postgresqlUser`            | Postgres username                           | `postgres`                                                   |\n| `postgresql.postgresqlHost`            | Postgres host                      | `nil`                                                      |\n| `postgresql.postgresqlPassword`        | Postgres password                  | `nil`                                                      |\n| `postgresql.existingSecret`            | Provide an existing `Secret` for postgres   | `nil`                                                      |\n| `postgresql.existingSecretKey`         | The postgres password key in the existing `Secret`   | `postgresql-password`                              |\n| `postgresql.existingSecretUserKey`     | The postgres username key in the existing `Secret`   | `postgresql-username`                            |\n| `postgresql.postgresqlPort`            | Postgres port                      | `5432`                                                     |\n| `postgresql.ssl`                       | Enable external postgres SSL connection     | `false`                                                   |\n| `postgresql.ca`                        | Certificate of Authority content for postgres  | `nil`                                                     |\n| `postgresql.persistence.enabled`                | Enable postgres persistence using PVC                | `true`                                                     |\n| `postgresql.persistence.existingClaim`          | Provide an existing `PersistentVolumeClaim` for postgres | `nil`                                                      |\n| `postgresql.persistence.storageClass`           | Postgres PVC Storage Class (example: `nfs`)                           | `nil`                 |\n| `postgresql.persistence.size`                   | Postgres PVC Storage Request                         | `8Gi`                                                     |\n| `postgresql.persistence.accessMode`             | Postgres Persistent Volume Access Mode                     | `ReadWriteOnce`                                          |\n| `postgresql.image.repository`                   | PostgreSQL image repository                       | `postgres`                                               |\n| `postgresql.image.tag`                          | PostgreSQL image tag                              | `18`                                                   |\n| `postgresql.image.pullPolicy`                   | PostgreSQL image pull policy                      | `IfNotPresent`                                           |\n| `postgresql.resources`                          | PostgreSQL resource requests/limits             | `{}`                                                     |\n| `postgresql.nodeSelector`                       | PostgreSQL node selector labels                   | `{}`                                                     |\n| `postgresql.tolerations`                        | PostgreSQL toleration labels                      | `[]`                                                     |\n| `postgresql.affinity`                           | PostgreSQL affinity settings                      | `{}`                                                     |\n| `postgresql.service.type`                       | PostgreSQL service type                           | `ClusterIP`                                              |\n| `postgresql.service.port`                       | PostgreSQL service port                           | `5432`                                                   |\n| `postgresql.service.annotations`                | PostgreSQL service annotations                    | `{}`                                                     |\n\nSpecify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,\n\n```console\n$ helm install --name my-release \\\n  --set postgresql.persistence.enabled=false \\\n   requarks/wiki\n```\n\nAlternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example,\n\n```console\n$ helm install --name my-release -f values.yaml requarks/wiki\n```\n\n> **Tip**: You can use the default [values.yaml](values.yaml)\n\n## PostgreSQL\n\nBy default, PostgreSQL is installed as part of the chart using the official PostgreSQL image from Docker Hub (version 18).\n\n### Using an external PostgreSQL server\n\nTo use an external PostgreSQL server, set `postgresql.enabled` to `false`, then use either:\n\n#### Connection String\n\nSet `externalPostgresql.databaseURL` to the full PostgreSQL connection string.\n\n#### Connection Parameters\n\nSet `externalPostgresql.host`, `externalPostgres.port`, `externalPostgres.database`, `externalPostgres.username`, `externalPostgres.existingSecret` *(secret name)* and `externalPostgres.existingSecretKey` *(key in the secret containing the password)*\n\nEnsure the secret specified in `externalPostgresql.existingSecret` already exists, with a password set at the path specified in `externalPostgres.existingSecretKey`.\n\nTo 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`.\n\n### Using an existing PostgreSQL secret with built-in PostgreSQL\n\nWhen 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:\n\n- `postgresql.existingSecret`: Name of the existing secret containing the credentials\n- `postgresql.existingSecretKey`: Key in the secret containing the password (defaults to `postgresql-password`)\n- `postgresql.existingSecretUserKey`: Key in the secret containing the username (defaults to `postgresql-username`)\nExample usage:\n```bash\n# Create your existing secret\nkubectl create secret generic my-postgres-secret \\\n  --from-literal=postgresql-username=postgres \\\n  --from-literal=postgresql-password=yourpassword\n\n# Deploy with existing secret\nhelm install my-release requarks/wiki \\\n  --set postgresql.enabled=true \\\n  --set postgresql.existingSecret=my-postgres-secret\n```\n\n## Persistence\n\nPersistent Volume Claims are used to keep the data across deployments. This is known to work in GCE, AWS, and minikube.\nSee the [Configuration](#configuration) section to configure the PVC or to disable persistence.\n\n## Ingress\n\nThis 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.\n\n## Extra Trusted Certificates\n\nTo append extra CA Certificates:\n\n1. Create a ConfigMap with CAs in PEM format, e.g.:\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: ca\n  namespace: your-wikijs-namespace\ndata:\n  certs.pem: |-\n    -----BEGIN CERTIFICATE-----\n    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n    -----END CERTIFICATE-----\n```\n\n2. 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.:\n\n```yaml\nvolumeMounts:\n  - name: ca\n    mountPath: /cas.pem\n    subPath: certs.pem\n\nvolumes:\n  - name: ca\n    configMap:\n      name: ca\n\nnodeExtraCaCerts: \"/cas.pem\"\n```\n"
  },
  {
    "path": "dev/helm/templates/NOTES.txt",
    "content": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingress.hosts }}\n  {{- range .paths }}\n  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}\n  {{- end }}\n{{- end }}\n{{- else if contains \"NodePort\" .Values.service.type }}\n  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath=\"{.spec.ports[0].nodePort}\" services {{ include \"wiki.fullname\" . }})\n  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath=\"{.items[0].status.addresses[0].address}\")\n  echo http://$NODE_IP:$NODE_PORT\n{{- else if contains \"LoadBalancer\" .Values.service.type }}\n     NOTE: It may take a few minutes for the LoadBalancer IP to be available.\n           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include \"wiki.fullname\" . }}'\n  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include \"wiki.fullname\" . }} --template \"{{\"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}\"}}\")\n  echo http://$SERVICE_IP:{{ .Values.service.port }}\n{{- else if contains \"ClusterIP\" .Values.service.type }}\n  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}\")\n  echo \"Visit http://127.0.0.1:8080 to use your application\"\n  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80\n{{- end }}\n\n{{- if .Values.postgresql.enabled }}\n2. PostgreSQL database has been deployed as part of this release:\n   - Database: {{ .Values.postgresql.postgresqlDatabase }}\n   - User: {{ .Values.postgresql.postgresqlUser }}\n   - Service: {{ include \"wiki.postgresql.fullname\" . }}\n   - Version: {{ .Values.postgresql.image.tag }}\n   - Persistence: {{ .Values.postgresql.persistence.enabled | ternary \"Enabled\" \"Disabled\" }}\n{{- end }}\n\n{{- if not .Values.postgresql.enabled }}\n2. External PostgreSQL setup detected. Ensure your database is accessible at the configured host.\n{{- end }}\n"
  },
  {
    "path": "dev/helm/templates/_helpers.tpl",
    "content": "{{/* vim: set filetype=mustache: */}}\n{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"wiki.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"wiki.fullname\" -}}\n{{- if .Values.fullnameOverride -}}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" -}}\n{{- else -}}\n{{- $name := default .Chart.Name .Values.nameOverride -}}\n{{- if contains $name .Release.Name -}}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" -}}\n{{- else -}}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"wiki.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"wiki.labels\" -}}\nhelm.sh/chart: {{ include \"wiki.chart\" . }}\n{{ include \"wiki.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end -}}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"wiki.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"wiki.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end -}}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"wiki.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create -}}\n    {{ default (include \"wiki.fullname\" .) .Values.serviceAccount.name }}\n{{- else -}}\n    {{ default \"default\" .Values.serviceAccount.name }}\n{{- end -}}\n{{- end -}}\n\n{{/*\nPostgreSQL fullname\n*/}}\n{{- define \"wiki.postgresql.fullname\" -}}\n{{- printf \"%s-%s\" (include \"wiki.fullname\" .) \"postgresql\" | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n\n{{/*\nPostgreSQL selector labels\n*/}}\n{{- define \"wiki.postgresql.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"wiki.name\" . }}-postgresql\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end -}}\n\n{{/*\nSet postgres host\n*/}}\n{{- define \"wiki.postgresql.host\" -}}\n{{- if .Values.postgresql.enabled -}}\n{{- include \"wiki.postgresql.fullname\" . -}}\n{{- else -}}\n{{- .Values.postgresql.postgresqlHost | default \"localhost\" | quote -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nSet postgres secret\n*/}}\n{{- define \"wiki.postgresql.secret\" -}}\n{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}\n    {{- .Values.postgresql.existingSecret -}}\n{{- else if .Values.postgresql.enabled -}}\n    {{- include \"wiki.postgresql.fullname\" . -}}\n{{- else -}}\n    {{- template \"wiki.fullname\" . -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nSet postgres secretUserKey\n*/}}\n{{- define \"wiki.postgresql.secretUserKey\" -}}\n{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}\n    {{- default \"postgresql-username\" .Values.postgresql.existingSecretUserKey | quote -}}\n{{- else if .Values.postgresql.enabled -}}\n    \"postgresql-username\"\n{{- else -}}\n    {{- default \"postgresql-username\" .Values.postgresql.existingSecretUserKey | quote -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nSet postgres secretKey\n*/}}\n{{- define \"wiki.postgresql.secretKey\" -}}\n{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}\n    {{- default \"postgresql-password\" .Values.postgresql.existingSecretKey | quote -}}\n{{- else if .Values.postgresql.enabled -}}\n    \"postgresql-password\"\n{{- else -}}\n    {{- default \"postgresql-password\" .Values.postgresql.existingSecretKey | quote -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nSet postgres secretDatabaseKey\n*/}}\n{{- define \"wiki.postgresql.secretDatabaseKey\" -}}\n{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}\n    {{- default \"postgresql-database\" .Values.postgresql.existingSecretDatabaseKey | quote -}}\n{{- else if .Values.postgresql.enabled -}}\n    \"postgresql-database\"\n{{- else -}}\n    {{- default \"postgresql-database\" .Values.postgresql.existingSecretDatabaseKey | quote -}}\n{{- end -}}\n{{- end -}}\n"
  },
  {
    "path": "dev/helm/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"wiki.fullname\" . }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}\n  selector:\n    matchLabels:\n      {{- include \"wiki.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"wiki.selectorLabels\" . | nindent 8 }}\n      annotations:\n        {{- toYaml .Values.podAnnotations | nindent 8 }}\n    spec:\n    {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n    {{- end }}\n      serviceAccountName: {{ include \"wiki.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      {{- if .Values.sideload.enabled }}\n      initContainers:\n        - name: {{ .Chart.Name }}-sideload\n          securityContext:\n            {{- toYaml .Values.sideload.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ default \"2\" .Values.image.tag }}\"\n          imagePullPolicy: {{ default \"IfNotPresent\" .Values.image.imagePullPolicy }}\n          env:\n            {{- toYaml .Values.sideload.env | nindent 12 }}\n          command: [ \"sh\", \"-c\" ]\n          args: [ \"mkdir -p /wiki/data/sideload && git clone --depth=1 {{ .Values.sideload.repoURL }} /wiki/data/sideload/\" ]\n          resources:\n            {{- toYaml .Values.sideload.resources | nindent 12 }}\n      {{- end }}\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ default \"2\" .Values.image.tag }}\"\n          imagePullPolicy: {{ default \"IfNotPresent\" .Values.image.imagePullPolicy }}\n          env:\n            {{- if .Values.nodeExtraCaCerts }}\n            - name: NODE_EXTRA_CA_CERTS\n              value: {{ .Values.nodeExtraCaCerts }}\n            {{- end }}\n            - name: DB_TYPE\n              value: postgres\n            {{- if and .Values.externalPostgresql .Values.externalPostgresql.databaseURL }}\n            - name: DATABASE_URL\n              value: {{ .Values.externalPostgresql.databaseURL }}\n            - name: NODE_TLS_REJECT_UNAUTHORIZED\n              value: {{ default \"1\" .Values.externalPostgresql.NODE_TLS_REJECT_UNAUTHORIZED | quote }}\n            {{- else if .Values.postgresql.enabled }}\n            - name: DB_HOST\n              value: {{ template \"wiki.postgresql.host\" . }}\n            - name: DB_PORT\n              value: \"{{ default \"5432\" .Values.postgresql.postgresqlPort }}\"\n            - name: DB_NAME\n              value: {{ default \"wiki\" .Values.postgresql.postgresqlDatabase | quote }}\n            - name: DB_USER\n            {{- if .Values.postgresql.existingSecret }}\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Values.postgresql.existingSecret }}\n                  key: {{ template \"wiki.postgresql.secretUserKey\" . }}\n            {{- else }}\n              value: {{ default \"postgres\" .Values.postgresql.postgresqlUser }}\n            {{- end }}\n            - name: DB_SSL\n              value: \"{{ default \"false\" .Values.postgresql.ssl }}\"\n            - name: DB_SSL_CA\n              value: \"{{ default \"\" .Values.postgresql.ca }}\"\n            - name: DB_PASS\n              valueFrom:\n                secretKeyRef:\n                  name: {{ template \"wiki.postgresql.secret\" . }}\n                  key: {{ template \"wiki.postgresql.secretKey\" . }}\n            {{- else if .Values.externalPostgresql }}\n            # External PostgreSQL configuration\n            - name: DB_HOST\n              value: {{ required \"External PostgreSQL host is required when postgresql.enabled is false\" .Values.externalPostgresql.host | quote }}\n            - name: DB_PORT\n              value: {{ required \"External PostgreSQL port is required when postgresql.enabled is false\" .Values.externalPostgresql.port | quote }}\n            - name: DB_NAME\n              value: {{ required \"External PostgreSQL database name is required when postgresql.enabled is false\" .Values.externalPostgresql.database | quote }}\n            - name: DB_USER\n              value: {{ required \"External PostgreSQL user is required when postgresql.enabled is false\" .Values.externalPostgresql.username | quote }}\n            - name: DB_PASS\n              valueFrom:\n                secretKeyRef:\n                  name: {{ required \"External PostgreSQL secret name is required when postgresql.enabled is false\" .Values.externalPostgresql.existingSecret | quote }}\n                  key: {{ required \"External PostgreSQL secret key is required when postgresql.enabled is false\" .Values.externalPostgresql.existingSecretKey | quote }}\n            - name: DB_SSL\n              value: \"{{ default \"false\" .Values.externalPostgresql.ssl }}\"\n            - name: DB_SSL_CA\n              value: \"{{ default \"\" .Values.externalPostgresql.ca }}\"\n            {{- end }}\n            - name: HA_ACTIVE\n              value: {{ .Values.replicaCount | int | le 2 | quote }}\n            {{- with .Values.extraEnvVars }}\n            {{- toYaml . | nindent 12 }}\n            {{- end }}\n    {{- with .Values.volumeMounts }}\n          volumeMounts:\n            {{- toYaml . | nindent 12 }}\n    {{- end }}\n          ports:\n            - name: http\n              containerPort: 3000\n              protocol: TCP\n          livenessProbe:\n            {{- toYaml .Values.livenessProbe | nindent 12 }}\n          readinessProbe:\n            {{- toYaml .Values.readinessProbe | nindent 12 }}\n          startupProbe:\n            {{- toYaml .Values.startupProbe | nindent 12 }}\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n    {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n    {{- end }}\n    {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n    {{- end }}\n    {{- with .Values.volumes }}\n      volumes:\n        {{- toYaml . | nindent 8 }}\n    {{- end }}\n"
  },
  {
    "path": "dev/helm/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\n  {{- $fullName := include \"wiki.fullname\" . -}}\n  {{- $svcPort := .Values.service.port -}}\n  {{- if and .Values.ingress.className (not (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion)) }}\n  {{- if not (hasKey .Values.ingress.annotations \"kubernetes.io/ingress.class\") }}\n  {{- $_ := set .Values.ingress.annotations \"kubernetes.io/ingress.class\" .Values.ingress.className}}\n  {{- end }}\n  {{- end }}\n  {{- if semverCompare \">=1.19-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1\n  {{- else if semverCompare \">=1.14-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1beta1\n  {{- else -}}\napiVersion: extensions/v1beta1\n  {{- end }}\nkind: Ingress\nmetadata:\n  name: {{ $fullName }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if and .Values.ingress.className (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion) }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n{{- if .Values.ingress.tls }}\n  tls:\n  {{- range .Values.ingress.tls }}\n    - hosts:\n      {{- range .hosts }}\n        - {{ . | quote }}\n      {{- end }}\n      secretName: {{ .secretName }}\n  {{- end }}\n{{- end }}\n  rules:\n  {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            {{- if and .pathType (semverCompare \">=1.18-0\" $.Capabilities.KubeVersion.GitVersion) }}\n            pathType: {{ .pathType }}\n            {{- end }}\n            backend:\n              {{- if semverCompare \">=1.19-0\" $.Capabilities.KubeVersion.GitVersion }}\n              service:\n                name: {{ $fullName }}\n                port:\n                  number: {{ $svcPort }}\n              {{- else }}\n              serviceName: {{ $fullName }}\n              servicePort: {{ $svcPort }}\n              {{- end }}\n          {{- end }}\n  {{- end }}\n  {{- end }}\n"
  },
  {
    "path": "dev/helm/templates/postgresql-pvc.yaml",
    "content": "{{- if and .Values.postgresql.enabled .Values.postgresql.persistence.enabled -}}\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: {{ include \"wiki.postgresql.fullname\" . }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\nspec:\n  accessModes:\n    - {{ .Values.postgresql.persistence.accessMode | quote }}\n  resources:\n    requests:\n      storage: {{ .Values.postgresql.persistence.size | quote }}\n  {{- if .Values.postgresql.persistence.storageClass }}\n  {{- if (eq \"-\" .Values.postgresql.persistence.storageClass) }}\n  storageClassName: \"\"\n  {{- else }}\n  storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }}\n  {{- end }}\n  {{- end }}\n{{- end }}"
  },
  {
    "path": "dev/helm/templates/postgresql-secret.yaml",
    "content": "{{- if and .Values.postgresql.enabled (not .Values.postgresql.existingSecret) -}}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"wiki.postgresql.fullname\" . }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\ntype: Opaque\ndata:\n  postgresql-password: {{ .Values.postgresql.postgresqlPassword | b64enc | quote }}\n  postgresql-username: {{ .Values.postgresql.postgresqlUser | b64enc | quote }}\n{{- end }}\n"
  },
  {
    "path": "dev/helm/templates/postgresql-service.yaml",
    "content": "{{- if .Values.postgresql.enabled -}}\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"wiki.postgresql.fullname\" . }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\n  {{- with .Values.postgresql.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.postgresql.service.type }}\n  ports:\n    - port: {{ .Values.postgresql.service.port }}\n      targetPort: 5432\n      protocol: TCP\n      name: postgresql\n  selector:\n    {{- include \"wiki.postgresql.selectorLabels\" . | nindent 4 }}\n{{- end }}"
  },
  {
    "path": "dev/helm/templates/postgresql-statefulset.yaml",
    "content": "{{- if .Values.postgresql.enabled -}}\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: {{ include \"wiki.postgresql.fullname\" . }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\nspec:\n  serviceName: {{ include \"wiki.postgresql.fullname\" . }}\n  replicas: 1\n  selector:\n    matchLabels:\n      {{- include \"wiki.postgresql.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"wiki.postgresql.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- with .Values.postgresql.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.postgresql.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.postgresql.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      containers:\n        - name: postgresql\n          image: {{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}\n          imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }}\n          ports:\n            - containerPort: 5432\n              name: postgresql\n          env:\n            - name: POSTGRES_DB\n              value: {{ .Values.postgresql.postgresqlDatabase | quote }}\n            - name: POSTGRES_USER\n            {{- if .Values.postgresql.existingSecret }}\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Values.postgresql.existingSecret }}\n                  key: {{ default \"postgresql-username\" .Values.postgresql.existingSecretUserKey | quote }}\n            {{- else }}\n              valueFrom:\n                secretKeyRef:\n                  name: {{ include \"wiki.postgresql.fullname\" . }}\n                  key: postgresql-username\n            {{- end }}\n            - name: POSTGRES_PASSWORD\n            {{- if .Values.postgresql.existingSecret }}\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Values.postgresql.existingSecret }}\n                  key: {{ default \"postgresql-password\" .Values.postgresql.existingSecretKey | quote }}\n            {{- else }}\n              valueFrom:\n                secretKeyRef:\n                  name: {{ include \"wiki.postgresql.fullname\" . }}\n                  key: postgresql-password\n            {{- end }}\n            - name: PGDATA\n              value: /var/lib/postgresql/data/pgdata\n          livenessProbe:\n            exec:\n              command:\n                - sh\n                - -c\n                - exec pg_isready -U {{ .Values.postgresql.postgresqlUser }} -d {{ .Values.postgresql.postgresqlDatabase }}\n            initialDelaySeconds: 60\n            periodSeconds: 10\n            timeoutSeconds: 5\n            failureThreshold: 6\n          readinessProbe:\n            exec:\n              command:\n                - sh\n                - -c\n                - exec pg_isready -U {{ .Values.postgresql.postgresqlUser }} -d {{ .Values.postgresql.postgresqlDatabase }}\n            initialDelaySeconds: 5\n            periodSeconds: 5\n            timeoutSeconds: 3\n            failureThreshold: 6\n          resources:\n            {{- toYaml .Values.postgresql.resources | nindent 12 }}\n          volumeMounts:\n            - name: postgresql-data\n              mountPath: /var/lib/postgresql/data\n              subPath: postgresql\n      volumes:\n        - name: postgresql-data\n        {{- if .Values.postgresql.persistence.enabled }}\n          persistentVolumeClaim:\n            claimName: {{ include \"wiki.postgresql.fullname\" . }}\n        {{- else }}\n          emptyDir: {}\n        {{- end }}\n{{- end }}\n"
  },
  {
    "path": "dev/helm/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{include \"wiki.fullname\" .}}\n  labels: {{- include \"wiki.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- range $key, $value := . }}\n      {{ $key }}: {{ $value | quote }}\n    {{- end }}\n  {{- end }}\nspec:\n  type: {{.Values.service.type}}\n  {{- if eq .Values.service.type \"LoadBalancer\" }}\n  loadBalancerIP: {{ default \"\" .Values.service.loadBalancerIP }}\n  {{- end }}\n  ports:\n    - port: {{ default \"80\" .Values.service.port}}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector: {{- include \"wiki.selectorLabels\" . | nindent 4}}\n"
  },
  {
    "path": "dev/helm/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"wiki.serviceAccountName\" . }}\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end -}}\n"
  },
  {
    "path": "dev/helm/templates/tests/test-connection.yaml",
    "content": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ include \"wiki.fullname\" . }}-test-connection\"\n  labels:\n    {{- include \"wiki.labels\" . | nindent 4 }}\n  annotations:\n    \"helm.sh/hook\": test-success\nspec:\n  containers:\n    - name: wget\n      image: busybox\n      command: ['wget']\n      args: ['{{ include \"wiki.fullname\" . }}:{{ .Values.service.port }}']\n  restartPolicy: Never\n"
  },
  {
    "path": "dev/helm/values.yaml",
    "content": "# Default values for wiki.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\nrevisionHistoryLimit: 2\n\nimage:\n  repository: requarks/wiki\n  imagePullPolicy: IfNotPresent\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: true\n  # Annotations to add to the service account\n  annotations: {}\n  # The name of the service account to use.\n  # If not set and create is true, a name is generated using the fullname template\n  name:\n\nlivenessProbe:\n  httpGet:\n    path: /healthz\n    port: http\n\nreadinessProbe:\n  httpGet:\n    path: /healthz\n    port: http\n\nstartupProbe:\n  initialDelaySeconds: 15\n  periodSeconds: 5\n  timeoutSeconds: 5\n  successThreshold: 1\n  failureThreshold: 60\n  httpGet:\n    path: /healthz\n    port: http\n\npodAnnotations: {}\n\npodSecurityContext: {}\n  # fsGroup: 2000\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\nservice:\n  type: ClusterIP\n  port: 80\n  # Annotations applied for services such as externalDNS or\n  # service type LoadBalancer\n  # type: LoadBalancer\n  # annotations: {}\n  # loadBalancerIP: 172.16.0.1\n\ningress:\n  enabled: true\n  className: \"\"\n  annotations: {}\n    # kubernetes.io/ingress.class: nginx\n    # kubernetes.io/tls-acme: \"true\"\n  hosts:\n    - host: wiki.minikube.local\n      paths:\n        - path: \"/\"\n          pathType: Prefix\n\n  tls: []\n  #  - secretName: chart-example-tls\n  #    hosts:\n  #      - chart-example.local\n\nresources: {}\n  # We usually recommend not to specify default resources and to leave this as a conscious\n  # choice for the user. This also increases chances charts run on environments with little\n  # resources, such as Minikube. If you do want to specify resources, uncomment the following\n  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n  # limits:\n  #   cpu: 100m\n  #   memory: 128Mi\n  # requests:\n  #   cpu: 100m\n  #   memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\nvolumeMounts: []\n\nvolumes: []\n\n# This will allow us to install locales even without internet access using a initContainer & Wiki.js \"sideloading\"\nsideload:\n  enabled: false\n  # Git-Repo containing all locales.json-files you need:\n  repoURL: https://github.com/requarks/wiki-localization\n\n  ## This can be helpfull if you have internet access over a http proxy:\n  env: []\n  #  - name: HTTPS_PROXY\n  #    value: http://my.proxy.com:3128\n\n  securityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\n  resources: {}\n  # We usually recommend not to specify default resources and to leave this as a conscious\n  # choice for the user. This also increases chances charts run on environments with little\n  # resources, such as Minikube. If you do want to specify resources, uncomment the following\n  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n  # limits:\n  #   cpu: 100m\n  #   memory: 128Mi\n  # requests:\n  #   cpu: 100m\n  #   memory: 128Mi\n\n## Append extra trusted certificates for node process from extra volume via NODE_EXTRA_CA_CERTS variable\n# nodeExtraCaCerts: \"/path/to/certs.pem\"\n\n## Additional environment variables to set\nextraEnvVars: []\n# extraEnvVars:\n#   - name: CUSTOM_VAR\n#     value: \"custom_value\"\n#   - name: SECRET_VAR\n#     valueFrom:\n#       secretKeyRef:\n#         name: my-secret\n#         key: secret-key\n\n## This will override the postgresql chart values\n# externalPostgresql:\n#   # note: ?sslmode=require => ?ssl=true\n#   databaseURL: postgresql://postgres:postgres@postgres:5432/wiki?ssl=true\n#   # For self signed CAs, like DigitalOcean\n#   NODE_TLS_REJECT_UNAUTHORIZED: \"0\"\n\n## Configuration for the custom PostgreSQL 18 deployment\n##\npostgresql:\n  enabled: true\n  ## ssl enforce SSL communication with PostgresSQL\n  ## Default to false\n  ##\n  ssl: false\n  ## ca Certificate of Authority\n  ## Default to empty, point to location of CA\n  ##\n  # ca: \"path to ca\"\n  ## postgresqlHost override postgres database host\n  ## Default to the service name of the custom PostgreSQL deployment\n  ##\n  postgresqlHost: \"{{ include \\\"wiki.postgresql.fullname\\\" . }}\"\n  ## postgresqlPort port for postgres\n  ## Default to 5432\n  ##\n  postgresqlPort: 5432\n  ## PostgreSQL User to create.\n  ##\n  postgresqlUser: postgres\n  ## PostgreSQL Database to create.\n  ##\n  postgresqlDatabase: wiki\n  ## PostgreSQL password (will be stored in a secret)\n  ##\n  postgresqlPassword: \"postgres\"\n\n  ## Use existing secret for PostgreSQL credentials\n  ## If set, the chart will not create a new secret and will use the existing one\n  ##\n  # existingSecret: \"my-existing-postgres-secret\"\n\n  ## Key in the existing secret containing the password\n  ##\n  # existingSecretKey: \"postgresql-password\"\n\n  ## Key in the existing secret containing the username (defaults to \"postgresql-username\")\n  ##\n  # existingSecretUserKey: \"postgresql-username\"\n\n  ## Persistent Volume Storage configuration.\n  ## ref: https://kubernetes.io/docs/user-guide/persistent-volumes\n  ##\n  persistence:\n    ## Enable PostgreSQL persistence using Persistent Volume Claims.\n    ##\n    enabled: true\n    ## concourse data Persistent Volume Storage Class\n    ## If defined, storageClassName: <storageClass>\n    ## If set to \"-\", storageClassName: \"\", which disables dynamic provisioning\n    ## If undefined (the default) or set to null, no storageClassName spec is\n    ##   set, choosing the default provisioner.  (gp2 on AWS, standard on\n    ##   GKE, AWS & OpenStack)\n    ##\n    # storageClass: \"-\"\n    ## Persistent Volume Access Mode.\n    ##\n    accessMode: ReadWriteOnce\n    ## Persistent Volume Storage Size.\n    ##\n    size: 8Gi\n\n  ## PostgreSQL Image Configuration\n  image:\n    repository: postgres\n    tag: \"18\"\n    pullPolicy: IfNotPresent\n\n  ## PostgreSQL Resources Configuration\n  resources: {}\n    # We usually recommend not to specify default resources and to leave this as a conscious\n    # choice for the user. This also increases chances charts run on environments with little\n    # resources, such as Minikube. If you do want to specify resources, uncomment the following\n    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n    # limits:\n    #   cpu: 100m\n    #   memory: 128Mi\n    # requests:\n    #   cpu: 100m\n    #   memory: 128Mi\n\n  ## PostgreSQL Node Selector, Tolerations and Affinity\n  nodeSelector: {}\n  tolerations: []\n  affinity: {}\n\n  ## PostgreSQL Service Configuration\n  service:\n    type: ClusterIP\n    port: 5432\n    # Additional service annotations\n    annotations: {}\n"
  },
  {
    "path": "dev/index.js",
    "content": "#!/usr/bin/env node\n\n// ===========================================\n// Wiki.js DEV UTILITY\n// Licensed under AGPLv3\n// ===========================================\n\nconst _ = require('lodash')\nconst chalk = require('chalk')\n\nconst init = {\n  dev() {\n    const webpack = require('webpack')\n    const chokidar = require('chokidar')\n\n    console.info(chalk.yellow.bold('--- ====================== ---'))\n    console.info(chalk.yellow.bold('--- Wiki.js DEVELOPER MODE ---'))\n    console.info(chalk.yellow.bold('--- ====================== ---'))\n\n    global.DEV = true\n    global.WP_CONFIG = require('./webpack/webpack.dev.js')\n    global.WP = webpack(global.WP_CONFIG)\n    global.WP_DEV = {\n      devMiddleware: require('webpack-dev-middleware')(global.WP, {\n        publicPath: global.WP_CONFIG.output.publicPath\n      }),\n      hotMiddleware: require('webpack-hot-middleware')(global.WP)\n    }\n    global.WP_DEV.devMiddleware.waitUntilValid(() => {\n      console.info(chalk.yellow.bold('>>> Starting Wiki.js in DEVELOPER mode...'))\n      require('../server')\n\n      process.stdin.setEncoding('utf8')\n      process.stdin.on('data', data => {\n        if (_.trim(data) === 'rs') {\n          console.warn(chalk.yellow.bold('--- >>>>>>>>>>>>>>>>>>>>>>>> ---'))\n          console.warn(chalk.yellow.bold('--- Manual restart requested ---'))\n          console.warn(chalk.yellow.bold('--- <<<<<<<<<<<<<<<<<<<<<<<< ---'))\n          this.reload()\n        }\n      })\n\n      const devWatcher = chokidar.watch([\n        './server',\n        '!./server/views/master.pug'\n      ], {\n        cwd: process.cwd(),\n        ignoreInitial: true,\n        atomic: 400\n      })\n      devWatcher.on('ready', () => {\n        devWatcher.on('all', _.debounce(() => {\n          console.warn(chalk.yellow.bold('--- >>>>>>>>>>>>>>>>>>>>>>>>>>>> ---'))\n          console.warn(chalk.yellow.bold('--- Changes detected: Restarting ---'))\n          console.warn(chalk.yellow.bold('--- <<<<<<<<<<<<<<<<<<<<<<<<<<<< ---'))\n          this.reload()\n        }, 500))\n      })\n    })\n  },\n  async reload() {\n    console.warn(chalk.yellow('--- Gracefully stopping server...'))\n    await global.WIKI.kernel.shutdown(true)\n\n    console.warn(chalk.yellow('--- Purging node modules cache...'))\n\n    global.WIKI = {}\n    Object.keys(require.cache).forEach(id => {\n      if (/[/\\\\]server[/\\\\]/.test(id)) {\n        delete require.cache[id]\n      }\n    })\n    Object.keys(module.constructor._pathCache).forEach(cacheKey => {\n      if (/[/\\\\]server[/\\\\]/.test(cacheKey)) {\n        delete module.constructor._pathCache[cacheKey]\n      }\n    })\n\n    console.warn(chalk.yellow('--- Unregistering process listeners...'))\n\n    process.removeAllListeners('unhandledRejection')\n    process.removeAllListeners('uncaughtException')\n\n    require('../server')\n  }\n}\n\ninit.dev()\n"
  },
  {
    "path": "dev/installer/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"github.com/bugsnag/bugsnag-go\"\n\t\"github.com/fatih/color\"\n\t\"gopkg.in/AlecAivazis/survey.v1\"\n)\n\nvar qs = []*survey.Question{\n\t{\n\t\tName: \"location\",\n\t\tPrompt: &survey.Input{\n\t\t\tMessage: \"Where do you want to install Wiki.js?\",\n\t\t\tDefault: \"./wiki\",\n\t\t},\n\t\tValidate: survey.Required,\n\t},\n\t{\n\t\tName: \"dbtype\",\n\t\tPrompt: &survey.Select{\n\t\t\tMessage: \"Select a DB Driver:\",\n\t\t\tOptions: []string{\"MariabDB\", \"MS SQL Server\", \"MySQL\", \"PostgreSQL\", \"SQLite\"},\n\t\t\tDefault: \"PostgreSQL\",\n\t\t},\n\t},\n\t{\n\t\tName: \"port\",\n\t\tPrompt: &survey.Input{\n\t\t\tMessage: \"Server Port:\",\n\t\t\tDefault: \"3000\",\n\t\t},\n\t},\n}\n\nfunc main() {\n\tbugsnag.Configure(bugsnag.Configuration{\n\t\tAPIKey:       \"37770b3b08864599fd47c4edba5aa656\",\n\t\tReleaseStage: \"dev\",\n\t})\n\n\tbold := color.New(color.FgWhite).Add(color.Bold)\n\n\tlogo := `\n  __    __ _ _    _    _\n / / /\\ \\ (_) | _(_)  (_)___\n \\ \\/  \\/ / | |/ / |  | / __|\n  \\  /\\  /| |   <| |_ | \\__ \\\n   \\/  \\/ |_|_|\\_\\_(_)/ |___/\n                    |__/\n  `\n\tcolor.Yellow(logo)\n\n\tbold.Println(\"\\nInstaller for Wiki.js 2.x\")\n\tfmt.Printf(\"%s-%s\\n\\n\", runtime.GOOS, runtime.GOARCH)\n\n\t// Check system requirements\n\n\tbold.Println(\"Verifying system requirements...\")\n\tCheckNodeJs()\n\tCheckRAM()\n\tfmt.Println()\n\n\t// the answers will be written to this struct\n\tanswers := struct {\n\t\tLocation string\n\t\tDBType   string `survey:\"dbtype\"`\n\t\tPort     int\n\t}{}\n\n\t// perform the questions\n\terr := survey.Ask(qs, &answers)\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\n\tfmt.Printf(\"%s chose %d.\", answers.Location, answers.Port)\n\n\t// Download archives...\n\n\tbold.Println(\"\\nDownloading packages...\")\n\n\t// uiprogress.Start()\n\t// bar := uiprogress.AddBar(100)\n\n\t// bar.AppendCompleted()\n\t// bar.PrependElapsed()\n\n\t// for bar.Incr() {\n\t// \ttime.Sleep(time.Millisecond * 20)\n\t// }\n\n\tfinish := `\n  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n  |                                                   |\n  |    Open http://localhost:3000/ in your browser    |\n  |    to complete the installation!                  |\n  |                                                   |\n  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n  `\n\tcolor.Yellow(\"\\n\\n\" + finish)\n\n\tfmt.Println(\"Press any key to continue.\")\n\tfmt.Scanln()\n}\n"
  },
  {
    "path": "dev/installer/syscheck.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os/exec\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/fatih/color\"\n\t\"github.com/pbnjay/memory\"\n)\n\nconst nodejsSemverRange = \">=8.11.4 <11.0.0\"\nconst ramMin = 768\n\n// CheckNodeJs checks if Node.js is installed and has minimal supported version\nfunc CheckNodeJs() bool {\n\tcmd := exec.Command(\"node\", \"-v\")\n\tcmdOutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tvalidRange := semver.MustParseRange(nodejsSemverRange)\n\tnodeVersion, err := semver.ParseTolerant(string(cmdOutput[:]))\n\tif !validRange(nodeVersion) {\n\t\tpanic(fmt.Errorf(color.RedString(\"Error: Installed Node.js version %s is not supported! %s\\n\"), nodeVersion, nodejsSemverRange))\n\t}\n\n\tfmt.Printf(color.GreenString(\"✔\")+\" Node.js %s: OK\\n\", nodeVersion.String())\n\n\treturn true\n}\n\n// CheckRAM checks if system total RAM meets requirements\nfunc CheckRAM() bool {\n\tvar totalRAM = memory.TotalMemory() / 1024 / 1024\n\tif totalRAM < ramMin {\n\t\tpanic(fmt.Errorf(color.RedString(\"Error: System does not meet RAM requirements. %s MB minimum.\\n\"), ramMin))\n\t}\n\n\tfmt.Printf(color.GreenString(\"✔\")+\" Total System RAM %d MB: OK\\n\", totalRAM)\n\n\treturn true\n}\n\n// CheckNetworkAccess checks if download server can be reached\nfunc CheckNetworkAccess() bool {\n\t// TODO\n\treturn true\n}\n"
  },
  {
    "path": "dev/openshift/Dockerfile",
    "content": "FROM requarks/wiki:2\n\nUSER root\n\nRUN chgrp -R 0 /wiki /logs && \\\n    chmod -R g=u /wiki /logs\n\nUSER 1001\n"
  },
  {
    "path": "dev/packer/digitalocean.json",
    "content": "{\n  \"variables\": {\n    \"do_api_token\": \"{{env `DIGITALOCEAN_API_TOKEN`}}\",\n    \"image_name\": \"wikijs-snapshot-{{timestamp}}\",\n    \"apt_packages\": \"software-properties-common\",\n    \"application_name\": \"Wiki.js\",\n    \"application_version\": \"{{env `WIKI_APP_VERSION`}}\"\n  },\n  \"sensitive-variables\": [\n    \"do_api_token\"\n  ],\n  \"builders\": [\n    {\n      \"type\": \"digitalocean\",\n      \"api_token\": \"{{user `do_api_token`}}\",\n      \"image\": \"ubuntu-24-04-x64\",\n      \"region\": \"tor1\",\n      \"size\": \"s-1vcpu-1gb\",\n      \"ssh_username\": \"root\",\n      \"snapshot_name\": \"{{user `image_name`}}\"\n    }\n  ],\n  \"provisioners\": [\n    {\n      \"type\": \"shell\",\n      \"inline\": [\n        \"cloud-init status --wait\"\n      ]\n    },\n    {\n      \"type\": \"file\",\n      \"source\": \"scripts/001-onboot.sh\",\n      \"destination\": \"/var/lib/cloud/scripts/per-instance/001-onboot.sh\"\n    },\n    {\n      \"type\": \"file\",\n      \"source\": \"scripts/099-one-click\",\n      \"destination\": \"/etc/update-motd.d/099-one-click\"\n    },\n    {\n      \"type\": \"shell\",\n      \"inline\": [\n        \"chmod +x /var/lib/cloud/scripts/per-instance/001-onboot.sh\",\n        \"chmod +x /etc/update-motd.d/099-one-click\"\n      ]\n    },\n    {\n      \"type\": \"shell\",\n      \"environment_vars\": [\n        \"DEBIAN_FRONTEND=noninteractive\",\n        \"LC_ALL=C\",\n        \"LANG=en_US.UTF-8\",\n        \"LC_CTYPE=en_US.UTF-8\"\n      ],\n      \"inline\": [\n        \"apt -qqy update\",\n        \"apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade\",\n        \"apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install {{user `apt_packages`}}\",\n        \"apt-get -qqy clean\"\n      ]\n    },\n    {\n      \"type\": \"shell\",\n      \"environment_vars\": [\n        \"application_name={{user `application_name`}}\",\n        \"application_version={{user `application_version`}}\",\n        \"docker_compose_version={{user `docker_compose_version`}}\",\n        \"DEBIAN_FRONTEND=noninteractive\",\n        \"LC_ALL=C\",\n        \"LANG=en_US.UTF-8\",\n        \"LC_CTYPE=en_US.UTF-8\"\n      ],\n      \"scripts\": [\n        \"scripts/010-docker.sh\",\n        \"scripts/011-ufw-docker.sh\",\n        \"scripts/020-force-ssh-logout.sh\",\n        \"scripts/900-cleanup.sh\",\n        \"scripts/999-img-check.sh\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "dev/packer/scripts/001-onboot.sh",
    "content": "#!/bin/bash\n\n# Generate PostgreSQL password\nopenssl rand -base64 32 > /etc/wiki/.db-secret\n\n# Start containers\nif [[ -z $DATABASE_URL ]]; then\n  docker start db\nfi\ndocker start wiki\ndocker start wiki-update-companion\n\n# Remove the ssh force logout command\nsed -e '/Match User root/d' \\\n    -e '/.*ForceCommand.*droplet.*/d' \\\n    -i /etc/ssh/sshd_config\n\nsystemctl restart ssh\n"
  },
  {
    "path": "dev/packer/scripts/010-docker.sh",
    "content": "#!/bin/bash\n\n# Add Docker's official GPG key:\nsudo install -m 0755 -d /etc/apt/keyrings\nsudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\nsudo chmod a+r /etc/apt/keyrings/docker.asc\n\n# Add the repository to Apt sources:\nsudo tee /etc/apt/sources.list.d/docker.sources <<EOF\nTypes: deb\nURIs: https://download.docker.com/linux/ubuntu\nSuites: $(. /etc/os-release && echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\")\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\nEOF\n\nsudo apt -qqy update\n\n# Install Docker\nsudo apt -qqy install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\nsystemctl enable docker\nsystemctl start docker\n\n# Setup containers\n\nmkdir -p /etc/wiki\n\ndocker network create wikinet\ndocker volume create pgdata\ndocker create --name=db -e POSTGRES_DB=wiki -e POSTGRES_USER=wiki -e POSTGRES_PASSWORD_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -v pgdata:/var/lib/postgresql/data --restart=unless-stopped -h db --network=wikinet postgres:17\ndocker create --name=wiki -e DB_TYPE=postgres -e DB_HOST=db -e DB_PORT=5432 -e DB_PASS_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -e DB_USER=wiki -e DB_NAME=wiki -e UPGRADE_COMPANION=1 --restart=unless-stopped -h wiki --network=wikinet -p 80:3000 -p 443:3443 ghcr.io/requarks/wiki:2\ndocker create --name=wiki-update-companion -v /var/run/docker.sock:/var/run/docker.sock:ro --restart=unless-stopped -h wiki-update-companion --network=wikinet ghcr.io/requarks/wiki-update-companion:latest\n"
  },
  {
    "path": "dev/packer/scripts/011-ufw-docker.sh",
    "content": "#!/bin/bash\n\nufw limit ssh\nufw allow http\nufw allow https\n\nufw --force enable\n\ncat /dev/null > /var/log/ufw.log\n"
  },
  {
    "path": "dev/packer/scripts/020-force-ssh-logout.sh",
    "content": "#!/bin/sh\n\ncat >> /etc/ssh/sshd_config <<EOM\nMatch User root\n        ForceCommand echo \"Please wait while we get your droplet ready...\"\nEOM\n"
  },
  {
    "path": "dev/packer/scripts/099-one-click",
    "content": "#!/bin/sh\n#\n# Configured as part of the DigitalOcean 1-Click Image build process\n\nmyip=$(hostname -I | awk '{print$1}')\ncat <<EOF\n********************************************************************************\n\nWelcome to Wiki.js's 1-Click DigitalOcean Droplet.\n\nTo keep this Droplet secure, the UFW firewall is enabled.\nAll ports are BLOCKED except 22 (SSH), 80 (Docker) and 443 (Docker).\n\n* The Wiki.js 1-Click DigitalOcean Quickstart guide is available at:\n  https://docs.requarks.io/install/digitalocean\n\n* Docker is installed and configured per Docker's recommendations:\n  https://docs.docker.com/engine/install/ubuntu/\n\nFor more information, visit https://docs.requarks.io/install/digitalocean\n\n********************************************************************************\nTo delete this message of the day: rm -rf $(readlink -f ${0})\nEOF\n"
  },
  {
    "path": "dev/packer/scripts/900-cleanup.sh",
    "content": "#!/bin/bash\n\n# DigitalOcean Marketplace Image Validation Tool\n# © 2021 DigitalOcean LLC.\n# This code is licensed under Apache 2.0 license (see LICENSE.md for details)\n\nset -o errexit\n\n# Ensure /tmp exists and has the proper permissions before\n# checking for security updates\n# https://github.com/digitalocean/marketplace-partners/issues/94\nif [[ ! -d /tmp ]]; then\n  mkdir /tmp\nfi\nchmod 1777 /tmp\n\nexport DEBIAN_FRONTEND=noninteractive\napt-get -y update\napt-get -y purge droplet-agent\nrm -rf /opt/digitalocean\napt-get -y autoremove\napt-get -y autoclean\n\nrm -rf /tmp/* /var/tmp/*\nhistory -c\ncat /dev/null > /root/.bash_history\nunset HISTFILE\nfind /var/log -mtime -1 -type f -exec truncate -s 0 {} \\;\nrm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????\nrm -rf /var/lib/cloud/instances/*\nrm -f /root/.ssh/authorized_keys /etc/ssh/*key*\ntouch /etc/ssh/revoked_keys\nchmod 600 /etc/ssh/revoked_keys\n\n# Securely erase the unused portion of the filesystem\nGREEN='\\033[0;32m'\nNC='\\033[0m'\nprintf \"\\n${GREEN}Writing zeros to the remaining disk space to securely\nerase the unused portion of the file system.\nDepending on your disk size this may take several minutes.\nThe secure erase will complete successfully when you see:${NC}\n    dd: writing to '/zerofile': No space left on device\\n\nBeginning secure erase now\\n\"\n\ndd if=/dev/zero of=/zerofile bs=4096 || rm /zerofile\n"
  },
  {
    "path": "dev/packer/scripts/999-img-check.sh",
    "content": "#!/bin/bash\n\n# DigitalOcean Marketplace Image Validation Tool\n# © 2021-2022 DigitalOcean LLC.\n# This code is licensed under Apache 2.0 license (see LICENSE.md for details)\n\nVERSION=\"v. 1.8.1\"\nRUNDATE=$( date )\n\n# Script should be run with SUDO\nif [ \"$EUID\" -ne 0 ]\n  then echo \"[Error] - This script must be run with sudo or as the root user.\"\n  exit 1\nfi\n\nSTATUS=0\nPASS=0\nWARN=0\nFAIL=0\n\n# $1 == command to check for\n# returns: 0 == true, 1 == false\ncmdExists() {\n    if command -v \"$1\" > /dev/null 2>&1; then\n        return 0\n    else\n        return 1\n    fi\n}\n\nfunction getDistro {\n    if [ -f /etc/os-release ]; then\n    # freedesktop.org and systemd\n    # shellcheck disable=SC1091\n    . /etc/os-release\n    OS=$NAME\n    VER=$VERSION_ID\nelif type lsb_release >/dev/null 2>&1; then\n    # linuxbase.org\n    OS=$(lsb_release -si)\n    VER=$(lsb_release -sr)\nelif [ -f /etc/lsb-release ]; then\n    # For some versions of Debian/Ubuntu without lsb_release command\n    # shellcheck disable=SC1091\n    . /etc/lsb-release\n    OS=$DISTRIB_ID\n    VER=$DISTRIB_RELEASE\nelif [ -f /etc/debian_version ]; then\n    # Older Debian/Ubuntu/etc.\n    OS=Debian\n    VER=$(cat /etc/debian_version)\nelif [ -f /etc/SuSe-release ]; then\n    # Older SuSE/etc.\n    :\nelif [ -f /etc/redhat-release ]; then\n    # Older Red Hat, CentOS, etc.\n    VER=$(cut -d\" \" -f3 < /etc/redhat-release | cut -d \".\" -f1)\n    d=$(cut -d\" \" -f1 < /etc/redhat-release | cut -d \".\" -f1)\n    if [[ $d == \"CentOS\" ]]; then\n      OS=\"CentOS Linux\"\n    fi\nelse\n    # Fall back to uname, e.g. \"Linux <version>\", also works for BSD, etc.\n    OS=$(uname -s)\n    VER=$(uname -r)\nfi\n}\nfunction loadPasswords {\nSHADOW=$(cat /etc/shadow)\n}\n\nfunction checkAgent {\n  # Check for the presence of the DO directory in the filesystem\n  if [ -d /opt/digitalocean ];then\n     echo -en \"\\e[41m[FAIL]\\e[0m DigitalOcean directory detected.\\n\"\n            ((FAIL++))\n            STATUS=2\n      if [[ $OS == \"CentOS Linux\" ]] || [[ $OS == \"CentOS Stream\" ]] || [[ $OS == \"Rocky Linux\" ]] || [[ $OS == \"AlmaLinux\" ]] || [[ $OS == \"CloudLinux\" ]]; then\n        echo \"To uninstall the agent: 'sudo yum remove droplet-agent'\"\n        echo \"To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'\"\n      elif [[ $OS == \"Ubuntu\" ]] || [[ $OS == \"Debian\" ]]; then\n        echo \"To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-agent'\"\n      fi\n  else\n    echo -en \"\\e[32m[PASS]\\e[0m DigitalOcean Monitoring agent was not found\\n\"\n    ((PASS++))\n  fi\n}\n\nfunction checkLogs {\n    cp_ignore=\"/var/log/cpanel-install.log\"\n    echo -en \"\\nChecking for log files in /var/log\\n\\n\"\n    # Check if there are log archives or log files that have not been recently cleared.\n    for f in /var/log/*-????????; do\n      [[ -e $f ]] || break\n      if [ \"${f}\" != \"${cp_ignore}\" ]; then\n        echo -en \"\\e[93m[WARN]\\e[0m Log archive ${f} found; Contents:\\n\"\n        cat \"${f}\"\n        ((WARN++))\n        if [[ $STATUS != 2 ]]; then\n            STATUS=1\n        fi\n      fi\n    done\n    for f in  /var/log/*.[0-9];do\n      [[ -e $f ]] || break\n        echo -en \"\\e[93m[WARN]\\e[0m Log archive ${f} found; Contents:\\n\"\n        cat \"${f}\"\n        ((WARN++))\n        if [[ $STATUS != 2 ]]; then\n            STATUS=1\n        fi\n    done\n    for f in /var/log/*.log; do\n      [[ -e $f ]] || break\n      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\n      if [ \"${f}\" != \"${cp_ignore}\" ]; then\n        echo -en \"\\e[93m[WARN]\\e[0m un-cleared log file, ${f} found; Contents:\\n\"\n        cat \"${f}\"\n        ((WARN++))\n        if [[ $STATUS != 2 ]]; then\n            STATUS=1\n        fi\n      fi\n      elif [[ \"${f}\" != '/var/log/lfd.log' && \"$(wc -c < \"${f}\")\" -gt 50 ]]; then\n      if [ \"${f}\" != \"${cp_ignore}\" ]; then\n        echo -en \"\\e[93m[WARN]\\e[0m un-cleared log file, ${f} found; Contents:\\n\"\n        cat \"${f}\"\n        ((WARN++))\n        if [[ $STATUS != 2 ]]; then\n            STATUS=1\n        fi\n      fi\n    fi\n    done\n}\nfunction checkTMP {\n  # Check the /tmp directory to ensure it is empty.  Warn on any files found.\n  return 1\n}\nfunction checkRoot {\n    user=\"root\"\n    uhome=\"/root\"\n    for usr in $SHADOW\n    do\n      IFS=':' read -r -a u <<< \"$usr\"\n      if [[ \"${u[0]}\" == \"${user}\" ]]; then\n        if [[ ${u[1]} == \"!\" ]] || [[ ${u[1]} == \"!!\" ]] || [[ ${u[1]} == \"*\" ]]; then\n            echo -en \"\\e[32m[PASS]\\e[0m User ${user} has no password set.\\n\"\n            ((PASS++))\n        else\n            echo -en \"\\e[41m[FAIL]\\e[0m User ${user} has a password set on their account.\\n\"\n            ((FAIL++))\n            STATUS=2\n        fi\n      fi\n    done\n    if [ -d ${uhome}/ ]; then\n            if [ -d ${uhome}/.ssh/ ]; then\n                if  ls ${uhome}/.ssh/*> /dev/null 2>&1; then\n                    for key in \"${uhome}\"/.ssh/*\n                        do\n                             if  [ \"${key}\" == \"${uhome}/.ssh/authorized_keys\" ]; then\n\n                                if [ \"$(wc -c < \"${key}\")\" -gt 50 ]; then\n                                    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\"\n                                    akey=$(cat \"${key}\")\n                                    echo \"File Contents:\"\n                                    echo \"$akey\"\n                                    echo \"--------------\"\n                                    ((FAIL++))\n                                    STATUS=2\n                                fi\n                            elif  [ \"${key}\" == \"${uhome}/.ssh/id_rsa\" ]; then\n                                if [ \"$(wc -c < \"${key}\")\" -gt 0 ]; then\n                                  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\"\n                                      akey=$(cat \"${key}\")\n                                      echo \"File Contents:\"\n                                      echo \"$akey\"\n                                      echo \"--------------\"\n                                      ((FAIL++))\n                                      STATUS=2\n                                else\n                                  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\"\n                                  ((WARN++))\n                                  if [[ $STATUS != 2 ]]; then\n                                    STATUS=1\n                                  fi\n                                fi\n                            elif  [ \"${key}\" != \"${uhome}/.ssh/known_hosts\" ]; then\n                                 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\"\n                                    ((WARN++))\n                                    if [[ $STATUS != 2 ]]; then\n                                        STATUS=1\n                                    fi\n                            else\n                                if [ \"$(wc -c < \"${key}\")\" -gt 50 ]; then\n                                    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\"\n                                    ((WARN++))\n                                    if [[ $STATUS != 2 ]]; then\n                                        STATUS=1\n                                    fi\n                                fi\n                            fi\n                        done\n                else\n                    echo -en \"\\e[32m[ OK ]\\e[0m User \\e[1m${user}\\e[0m has no SSH keys present\\n\"\n                fi\n            else\n                echo -en \"\\e[32m[ OK ]\\e[0m User \\e[1m${user}\\e[0m does not have an .ssh directory\\n\"\n            fi\n             if [ -f /root/.bash_history ];then\n\n                      BH_S=$(wc -c < /root/.bash_history)\n\n                      if [[ $BH_S -lt 200 ]]; then\n                          echo -en \"\\e[32m[PASS]\\e[0m ${user}'s Bash History appears to have been cleared\\n\"\n                          ((PASS++))\n                      else\n                          echo -en \"\\e[41m[FAIL]\\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\\n\"\n                          ((FAIL++))\n                              STATUS=2\n                      fi\n\n                      return 1;\n                  else\n                      echo -en \"\\e[32m[PASS]\\e[0m The Root User's Bash History is not present\\n\"\n                      ((PASS++))\n                  fi\n        else\n            echo -en \"\\e[32m[ OK ]\\e[0m User \\e[1m${user}\\e[0m does not have a directory in /home\\n\"\n        fi\n        echo -en \"\\n\\n\"\n    return 1\n}\n\nfunction checkUsers {\n    # Check each user-created account\n    awk -F: '$3 >= 1000 && $1 != \"nobody\" {print $1}' < /etc/passwd | while IFS= read -r user;\n    do\n      # Skip some other non-user system accounts\n      if [[ $user == \"centos\" ]]; then\n        :\n      elif [[ $user == \"nfsnobody\" ]]; then\n        :\n    else\n      echo -en \"\\nChecking user: ${user}...\\n\"\n      for usr in $SHADOW\n        do\n          IFS=':' read -r -a u <<< \"$usr\"\n          if [[ \"${u[0]}\" == \"${user}\" ]]; then\n              if [[ ${u[1]} == \"!\" ]] || [[ ${u[1]} == \"!!\" ]] || [[ ${u[1]} == \"*\" ]]; then\n                  echo -en \"\\e[32m[PASS]\\e[0m User ${user} has no password set.\\n\"\n                  # shellcheck disable=SC2030\n                  ((PASS++))\n              else\n                  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\"\n                  # shellcheck disable=SC2030\n                  ((FAIL++))\n                  STATUS=2\n              fi\n          fi\n        done\n        #echo \"User Found: ${user}\"\n        uhome=\"/home/${user}\"\n        if [ -d \"${uhome}/\" ]; then\n            if [ -d \"${uhome}/.ssh/\" ]; then\n                if  ls \"${uhome}/.ssh/*\"> /dev/null 2>&1; then\n                    for key in \"${uhome}\"/.ssh/*\n                        do\n                            if  [ \"${key}\" == \"${uhome}/.ssh/authorized_keys\" ]; then\n                                if [ \"$(wc -c < \"${key}\")\" -gt 50 ]; then\n                                    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\"\n                                    akey=$(cat \"${key}\")\n                                    echo \"File Contents:\"\n                                    echo \"$akey\"\n                                    echo \"--------------\"\n                                    ((FAIL++))\n                                    STATUS=2\n                                fi\n                              elif  [ \"${key}\" == \"${uhome}/.ssh/id_rsa\" ]; then\n                                if [ \"$(wc -c < \"${key}\")\" -gt 0 ]; then\n                                  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\"\n                                      akey=$(cat \"${key}\")\n                                      echo \"File Contents:\"\n                                      echo \"$akey\"\n                                      echo \"--------------\"\n                                      ((FAIL++))\n                                      STATUS=2\n                                else\n                                  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\"\n                                  # shellcheck disable=SC2030\n                                  ((WARN++))\n                                  if [[ $STATUS != 2 ]]; then\n                                    STATUS=1\n                                  fi\n                                fi\n                            elif  [ \"${key}\" != \"${uhome}/.ssh/known_hosts\" ]; then\n\n                                 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\"\n                                 ((WARN++))\n                                 if [[ $STATUS != 2 ]]; then\n                                        STATUS=1\n                                    fi\n\n                            else\n                                if [ \"$(wc -c < \"${key}\")\" -gt 50 ]; then\n                                    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\"\n                                    ((WARN++))\n                                    if [[ $STATUS != 2 ]]; then\n                                        STATUS=1\n                                    fi\n                                fi\n                            fi\n\n\n                        done\n                else\n                    echo -en \"\\e[32m[ OK ]\\e[0m User \\e[1m${user}\\e[0m has no SSH keys present\\n\"\n                fi\n            else\n                echo -en \"\\e[32m[ OK ]\\e[0m User \\e[1m${user}\\e[0m does not have an .ssh directory\\n\"\n            fi\n        else\n            echo -en \"\\e[32m[ OK ]\\e[0m User \\e[1m${user}\\e[0m does not have a directory in /home\\n\"\n        fi\n\n         # Check for an uncleared .bash_history for this user\n              if [ -f \"${uhome}/.bash_history\" ]; then\n                            BH_S=$(wc -c < \"${uhome}/.bash_history\")\n\n                            if [[ $BH_S -lt 200 ]]; then\n                                echo -en \"\\e[32m[PASS]\\e[0m ${user}'s Bash History appears to have been cleared\\n\"\n                                ((PASS++))\n                            else\n                                echo -en \"\\e[41m[FAIL]\\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\\n\"\n                                ((FAIL++))\n                                    STATUS=2\n\n                            fi\n                           echo -en \"\\n\\n\"\n                         fi\n        fi\n    done\n}\nfunction checkFirewall {\n\n    if [[ $OS == \"Ubuntu\" ]]; then\n      fw=\"ufw\"\n      ufwa=$(ufw status |head -1| sed -e \"s/^Status:\\ //\")\n      if [[ $ufwa == \"active\" ]]; then\n        FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n        # shellcheck disable=SC2031\n        ((PASS++))\n      else\n        FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n        # shellcheck disable=SC2031\n        ((WARN++))\n      fi\n    elif [[ $OS == \"CentOS Linux\" ]] || [[ $OS == \"CentOS Stream\" ]] || [[ $OS == \"Rocky Linux\" ]] || [[ $OS == \"AlmaLinux\" ]] || [[ $OS == \"CloudLinux\" ]]; then\n      if [ -f /usr/lib/systemd/system/csf.service ]; then\n        fw=\"csf\"\n        if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then\n\n        FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n        ((PASS++))\n        elif cmdExists \"firewall-cmd\"; then\n          if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then\n           FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n          ((PASS++))\n          else\n            FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n          ((WARN++))\n          fi\n        else\n          FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n        ((WARN++))\n        fi\n      else\n        fw=\"firewalld\"\n        if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then\n          FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n        ((PASS++))\n        else\n          FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n        ((WARN++))\n        fi\n      fi\n    elif [[ \"$OS\" =~ Debian.* ]]; then\n      # user could be using a number of different services for managing their firewall\n      # we will check some of the most common\n      if cmdExists 'ufw'; then\n        fw=\"ufw\"\n        ufwa=$(ufw status |head -1| sed -e \"s/^Status:\\ //\")\n        if [[ $ufwa == \"active\" ]]; then\n        FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n        ((PASS++))\n      else\n        FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n        ((WARN++))\n      fi\n      elif cmdExists \"firewall-cmd\"; then\n        fw=\"firewalld\"\n        if [[ $(systemctl is-active --quiet $fw) ]]; then\n          FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n        ((PASS++))\n        else\n          FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n        ((WARN++))\n        fi\n      else\n        # user could be using vanilla iptables, check if kernel module is loaded\n        fw=\"iptables\"\n        if lsmod | grep -q '^ip_tables' 2>/dev/null; then\n          FW_VER=\"\\e[32m[PASS]\\e[0m Firewall service (${fw}) is active\\n\"\n        ((PASS++))\n        else\n          FW_VER=\"\\e[93m[WARN]\\e[0m No firewall is configured. Ensure ${fw} is installed and configured\\n\"\n        ((WARN++))\n        fi\n      fi\n    fi\n\n}\nfunction checkUpdates {\n    if [[ $OS == \"Ubuntu\" ]] || [[ \"$OS\" =~ Debian.* ]]; then\n        # Ensure /tmp exists and has the proper permissions before\n        # checking for security updates\n        # https://github.com/digitalocean/marketplace-partners/issues/94\n        if [[ ! -d /tmp ]]; then\n          mkdir /tmp\n        fi\n        chmod 1777 /tmp\n\n        echo -en \"\\nUpdating apt package database to check for security updates, this may take a minute...\\n\\n\"\n        apt-get -y update > /dev/null\n\n        uc=$(apt-get --just-print upgrade | grep -i \"security\" -c)\n        if [[ $uc -gt 0 ]]; then\n          update_count=$(( uc / 2 ))\n        else\n          update_count=0\n        fi\n\n        if [[ $update_count -gt 0 ]]; then\n            echo -en \"\\e[41m[FAIL]\\e[0m There are ${update_count} security updates available for this image that have not been installed.\\n\"\n            echo -en\n            echo -en \"Here is a list of the security updates that are not installed:\\n\"\n            sleep 2\n            apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'\n            echo -en\n            # shellcheck disable=SC2031\n            ((FAIL++))\n            STATUS=2\n        else\n            echo -en \"\\e[32m[PASS]\\e[0m There are no pending security updates for this image.\\n\\n\"\n            ((PASS++))\n        fi\n    elif [[ $OS == \"CentOS Linux\" ]] || [[ $OS == \"CentOS Stream\" ]] || [[ $OS == \"Rocky Linux\" ]] || [[ $OS == \"AlmaLinux\" ]] || [[ $OS == \"CloudLinux\" ]]; then\n        echo -en \"\\nChecking for available security updates, this may take a minute...\\n\\n\"\n\n        update_count=$(yum check-update --security --quiet | wc -l)\n         if [[ $update_count -gt 0 ]]; then\n            echo -en \"\\e[41m[FAIL]\\e[0m There are ${update_count} security updates available for this image that have not been installed.\\n\"\n            ((FAIL++))\n            STATUS=2\n        else\n            echo -en \"\\e[32m[PASS]\\e[0m There are no pending security updates for this image.\\n\"\n            ((PASS++))\n        fi\n    else\n        echo \"Error encountered\"\n        exit 1\n    fi\n\n    return 1;\n}\nfunction checkCloudInit {\n\n    if hash cloud-init 2>/dev/null; then\n        CI=\"\\e[32m[PASS]\\e[0m Cloud-init is installed.\\n\"\n        ((PASS++))\n    else\n        CI=\"\\e[41m[FAIL]\\e[0m No valid verison of cloud-init was found.\\n\"\n        ((FAIL++))\n        STATUS=2\n    fi\n    return 1\n}\n\nfunction version_gt() { test \"$(printf '%s\\n' \"$@\" | sort -V | head -n 1)\" != \"$1\"; }\n\n\nclear\necho \"DigitalOcean Marketplace Image Validation Tool ${VERSION}\"\necho \"Executed on: ${RUNDATE}\"\necho \"Checking local system for Marketplace compatibility...\"\n\ngetDistro\n\necho -en \"\\n\\e[1mDistribution:\\e[0m ${OS}\\n\"\necho -en \"\\e[1mVersion:\\e[0m ${VER}\\n\\n\"\n\nost=0\nosv=0\n\nif [[ $OS == \"Ubuntu\" ]]; then\n        ost=1\n    if [[ $VER == \"24.04\" ]] || [[ $VER == \"22.10\" ]] || [[ $VER == \"22.04\" ]] || [[ $VER == \"20.04\" ]] || [[ $VER == \"18.04\" ]] || [[ $VER == \"16.04\" ]]; then\n        osv=1\n    fi\n\nelif [[ \"$OS\" =~ Debian.* ]]; then\n    ost=1\n    case \"$VER\" in\n        9)\n            osv=1\n            ;;\n        10)\n            osv=1\n            ;;\n        11)\n            osv=1\n            ;;\n        12)\n            osv=1\n            ;;\n        13)\n            osv=1\n            ;;\n        *)\n            osv=2\n            ;;\n    esac\n\nelif [[ $OS == \"CentOS Linux\" ]]; then\n        ost=1\n    if [[ $VER == \"8\" ]]; then\n        osv=1\n    elif [[ $VER == \"7\" ]]; then\n        osv=1\n    elif [[ $VER == \"6\" ]]; then\n        osv=1\n    else\n        osv=2\n    fi\nelif [[ $OS == \"CentOS Stream\" ]]; then\n        ost=1\n    if [[ $VER == \"8\" ]]; then\n        osv=1\n    elif [[ $VER == \"9\" ]]; then\n        osv=1\n    else\n        osv=2\n    fi\nelif [[ $OS == \"Rocky Linux\" ]]; then\n        ost=1\n    if [[ $VER =~ 8\\. ]] || [[ $VER =~ 9\\. ]]; then\n        osv=1\n    else\n        osv=2\n    fi\nelif [[ $OS == \"AlmaLinux\" ]]; then\n        ost=1\n    if [[ \"$VER\" =~ 8.* ]] || [[ \"$VER\" =~ 9.* ]]; then\n        osv=1\n    else\n        osv=2\n    fi\nelif [[ $OS == \"CloudLinux\" ]]; then\n        ost=1\n    if [[ \"$VER\" =~ 8.* ]] || [[ \"$VER\" =~ 9.* ]]; then\n        osv=1\n    else\n        osv=2\n    fi\nelse\n    ost=0\nfi\n\nif [[ $ost == 1 ]]; then\n    echo -en \"\\e[32m[PASS]\\e[0m Supported Operating System Detected: ${OS}\\n\"\n    ((PASS++))\nelse\n    echo -en \"\\e[41m[FAIL]\\e[0m ${OS} is not a supported Operating System\\n\"\n    ((FAIL++))\n    STATUS=2\nfi\n\nif [[ $osv == 1 ]]; then\n    echo -en \"\\e[32m[PASS]\\e[0m Supported Release Detected: ${VER}\\n\"\n    ((PASS++))\nelif [[ $ost == 1 ]]; then\n    echo -en \"\\e[41m[FAIL]\\e[0m ${OS} ${VER} is not a supported Operating System Version\\n\"\n    ((FAIL++))\n    STATUS=2\nelse\n    echo \"Exiting...\"\n    exit 1\nfi\n\ncheckCloudInit\n\necho -en \"${CI}\"\n\ncheckFirewall\n\necho -en \"${FW_VER}\"\n\ncheckUpdates\n\nloadPasswords\n\ncheckLogs\n\necho -en \"\\n\\nChecking all user-created accounts...\\n\"\ncheckUsers\n\necho -en \"\\n\\nChecking the root account...\\n\"\ncheckRoot\n\ncheckAgent\n\n# Source GPU compatibility check\nif [ -f \"$(dirname \"$0\")/check_gpu_support.sh\" ]; then\n    source \"$(dirname \"$0\")/check_gpu_support.sh\"\nelse\n    echo \"GPU check script not found. Skipping GPU compatibility checks.\"\nfi\n\n# Summary\necho -en \"\\n\\n---------------------------------------------------------------------------------------------------\\n\"\n\nif [[ $STATUS == 0 ]]; then\n    echo -en \"Scan Complete.\\n\\e[32mAll Tests Passed!\\e[0m\\n\"\nelif [[ $STATUS == 1 ]]; then\n    echo -en \"Scan Complete. \\n\\e[93mSome non-critical tests failed.  Please review these items.\\e[0m\\e[0m\\n\"\nelse\n    echo -en \"Scan Complete. \\n\\e[41mOne or more tests failed.  Please review these items and re-test.\\e[0m\\n\"\nfi\necho \"---------------------------------------------------------------------------------------------------\"\necho -en \"\\e[1m${PASS} Tests PASSED\\e[0m\\n\"\necho -en \"\\e[1m${WARN} WARNINGS\\e[0m\\n\"\necho -en \"\\e[1m${FAIL} Tests FAILED\\e[0m\\n\"\necho -en \"---------------------------------------------------------------------------------------------------\\n\"\n\nif [[ $STATUS == 0 ]]; then\n    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\"\n    exit 0\nelif [[ $STATUS == 1 ]]; then\n    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\"\n    exit 0\nelse\n    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\"\n    exit 1\nfi\n"
  },
  {
    "path": "dev/search-engines/solr/solrconfig.xml",
    "content": "<config>\n</config>\n"
  },
  {
    "path": "dev/templates/legacy.pug",
    "content": "doctype html\nhtml\n  head\n    meta(http-equiv='X-UA-Compatible', content='IE=edge')\n    meta(charset='UTF-8')\n    meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5')\n    meta(name='theme-color', content='#1976d2')\n    meta(name='msapplication-TileColor', content='#1976d2')\n    meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png')\n\n    title= pageMeta.title + ' | ' + config.title\n\n    //- SEO / OpenGraph\n    meta(name='description', content=pageMeta.description)\n    meta(property='og:title', content=pageMeta.title)\n    meta(property='og:type', content='website')\n    meta(property='og:description', content=pageMeta.description)\n    meta(property='og:image', content=pageMeta.image)\n    meta(property='og:url', content=pageMeta.url)\n    meta(property='og:site_name', content=config.title)\n\n    //- Favicon\n    link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png')\n    link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-icon-192x192.png')\n    link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png')\n    link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png')\n    link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2')\n    link(rel='manifest', href='/_assets/manifest.json')\n\n    //- Icon Set\n    if config.theming.iconset === 'fa'\n      link(\n        type='text/css'\n        rel='stylesheet'\n        href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'\n        )\n    else if config.theming.iconset === 'fa4'\n      link(\n        type='text/css'\n        rel='stylesheet'\n        href='https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css'\n        )\n\n    //- CSS\n    <% for (var index in htmlWebpackPlugin.files.css) { %>\n    link(\n      type='text/css'\n      rel='stylesheet'\n      href='<%= htmlWebpackPlugin.files.css[index] %>'\n    )\n    <% } %>\n\n    script(\n      crossorigin='anonymous'\n      src='https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=EventSource'\n    )\n\n    //- JS\n    <% for (var index in htmlWebpackPlugin.files.js) { %>\n    script(\n      type='text/javascript'\n      src='<%= htmlWebpackPlugin.files.js[index] %>'\n      )\n    <% } %>\n\n    != analyticsCode.head\n\n    block head\n\n  body\n    != analyticsCode.bodyStart\n    block body\n    != analyticsCode.bodyEnd\n"
  },
  {
    "path": "dev/templates/master.pug",
    "content": "doctype html\nhtml(lang=siteConfig.lang)\n  head\n    meta(http-equiv='X-UA-Compatible', content='IE=edge')\n    meta(charset='UTF-8')\n    meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5')\n    meta(name='theme-color', content='#1976d2')\n    meta(name='msapplication-TileColor', content='#1976d2')\n    meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png')\n\n    title= pageMeta.title + ' | ' + config.title\n\n    //- SEO / OpenGraph\n    meta(name='description', content=pageMeta.description)\n    meta(property='og:title', content=pageMeta.title)\n    meta(property='og:type', content='website')\n    meta(property='og:description', content=pageMeta.description)\n    meta(property='og:image', content=pageMeta.image)\n    meta(property='og:url', content=pageMeta.url)\n    meta(property='og:site_name', content=config.title)\n\n    //- Favicon\n    link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png')\n    link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-chrome-192x192.png')\n    link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png')\n    link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png')\n    link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2')\n    link(rel='manifest', href='/_assets/manifest.json')\n\n    //- Site Properties\n    script.\n      var siteConfig = !{JSON.stringify(siteConfig)}\n      var siteLangs = !{JSON.stringify(langs)}\n\n    //- Dev Mode Warning\n    if devMode\n      script.\n        siteConfig.devMode = true\n\n    //- Icon Set\n    if config.theming.iconset === 'fa'\n      link(\n        type='text/css'\n        rel='stylesheet'\n        href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'\n        )\n    else if config.theming.iconset === 'fa4'\n      link(\n        type='text/css'\n        rel='stylesheet'\n        href='https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css'\n        )\n\n    //- CSS\n    <% for (var index in htmlWebpackPlugin.files.css) { %>\n      <% if (htmlWebpackPlugin.files.cssIntegrity) { %>\n    link(\n      type='text/css'\n      rel='stylesheet'\n      href='<%= htmlWebpackPlugin.files.css[index] %>'\n      integrity=config.security.securitySRI ? '<%= htmlWebpackPlugin.files.cssIntegrity[index] %>' : false\n      crossorigin='<%= webpackConfig.output.crossOriginLoading %>'\n    )\n      <% } else { %>\n    link(\n      type='text/css'\n      rel='stylesheet'\n      href='<%= htmlWebpackPlugin.files.css[index] %>'\n    )\n      <% } %>\n    <% } %>\n\n    //- JS\n    <% for (var index in htmlWebpackPlugin.files.js) { %>\n      <% if (htmlWebpackPlugin.files.jsIntegrity) { %>\n    script(\n      type='text/javascript'\n      src='<%= htmlWebpackPlugin.files.js[index] %>'\n      integrity=config.security.securitySRI ? '<%= htmlWebpackPlugin.files.jsIntegrity[index] %>' : false\n      crossorigin='<%= webpackConfig.output.crossOriginLoading %>'\n      )\n      <% } else { %>\n    script(\n      type='text/javascript'\n      src='<%= htmlWebpackPlugin.files.js[index] %>'\n      )\n      <% } %>\n    <% } %>\n\n    != analyticsCode.head\n\n    block head\n\n  body\n    != analyticsCode.bodyStart\n    block body\n    != analyticsCode.bodyEnd\n"
  },
  {
    "path": "dev/templates/setup.pug",
    "content": "doctype html\nhtml\n  head\n    meta(http-equiv='X-UA-Compatible', content='IE=edge')\n    meta(charset='UTF-8')\n    meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5')\n    meta(name='theme-color', content='#1976d2')\n    meta(name='msapplication-TileColor', content='#1976d2')\n    meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png')\n    title Wiki.js Setup\n\n    //- Favicon\n    link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png')\n    link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-chrome-192x192.png')\n    link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png')\n    link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png')\n    link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2')\n    link(rel='manifest', href='/_assets/manifest.json')\n\n    //- Site Lang\n    script.\n      var siteConfig = !{JSON.stringify({ title: config.title })}\n\n    //- Dev Mode Warning\n    if devMode\n      script.\n        siteConfig.devMode = true\n\n    //- CSS\n    <% for (var index in htmlWebpackPlugin.files.css) { %>\n    link(\n      type='text/css'\n      rel='stylesheet'\n      href='<%= htmlWebpackPlugin.files.css[index] %>'\n    )\n    <% } %>\n\n    //- JS\n    <% for (var index in htmlWebpackPlugin.files.js) { %>\n    script(\n      type='text/javascript'\n      src='<%= htmlWebpackPlugin.files.js[index] %>'\n      )\n    <% } %>\n\n  body\n    #root\n      setup(telemetry-id=telemetryClientID, wiki-version=packageObj.version)\n"
  },
  {
    "path": "dev/webpack/webpack.dev.js",
    "content": "const webpack = require('webpack')\nconst path = require('path')\nconst fs = require('fs-extra')\nconst yargs = require('yargs').argv\nconst _ = require('lodash')\n\nconst { VueLoaderPlugin } = require('vue-loader')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst HtmlWebpackPugPlugin = require('html-webpack-pug-plugin')\nconst MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin')\nconst VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')\nconst WriteFilePlugin = require('write-file-webpack-plugin')\nconst WebpackBarPlugin = require('webpackbar')\n\nconst babelConfig = fs.readJsonSync(path.join(process.cwd(), '.babelrc'))\nconst cacheDir = '.webpack-cache/cache'\nconst babelDir = path.join(process.cwd(), '.webpack-cache/babel')\n\nprocess.noDeprecation = true\n\nfs.emptyDirSync(path.join(process.cwd(), 'assets'))\n\nmodule.exports = {\n  mode: 'development',\n  entry: {\n    app: ['./client/index-app.js', 'webpack-hot-middleware/client'],\n    legacy: ['./client/index-legacy.js', 'webpack-hot-middleware/client'],\n    setup: ['./client/index-setup.js', 'webpack-hot-middleware/client']\n  },\n  output: {\n    path: path.join(process.cwd(), 'assets'),\n    publicPath: '/_assets/',\n    filename: 'js/[name].js',\n    chunkFilename: 'js/[name].js',\n    globalObject: 'this',\n    pathinfo: true,\n    crossOriginLoading: 'use-credentials'\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: (modulePath) => {\n          return modulePath.includes('node_modules') && !modulePath.includes('vuetify')\n        },\n        use: [\n          {\n            loader: 'cache-loader',\n            options: {\n              cacheDirectory: cacheDir\n            }\n          },\n          {\n            loader: 'babel-loader',\n            options: {\n              ...babelConfig,\n              cacheDirectory: babelDir\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.css$/,\n        use: [\n          'style-loader',\n          'css-loader',\n          'postcss-loader'\n        ]\n      },\n      {\n        test: /\\.sass$/,\n        use: [\n          {\n            loader: 'cache-loader',\n            options: {\n              cacheDirectory: cacheDir\n            }\n          },\n          'style-loader',\n          'css-loader',\n          'postcss-loader',\n          {\n            loader: 'sass-loader',\n            options: {\n              implementation: require('sass'),\n              sourceMap: false,\n              sassOptions: {\n                fiber: false\n              }\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n          {\n            loader: 'cache-loader',\n            options: {\n              cacheDirectory: cacheDir\n            }\n          },\n          'style-loader',\n          'css-loader',\n          'postcss-loader',\n          {\n            loader: 'sass-loader',\n            options: {\n              implementation: require('sass'),\n              sourceMap: false,\n              sassOptions: {\n                fiber: false\n              }\n            }\n          },\n          {\n            loader: 'sass-resources-loader',\n            options: {\n              resources: path.join(process.cwd(), '/client/scss/global.scss')\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader'\n      },\n      {\n        test: /\\.pug$/,\n        exclude: [\n          path.join(process.cwd(), 'dev')\n        ],\n        loader: 'pug-plain-loader'\n      },\n      {\n        test: /\\.(png|jpg|gif)$/,\n        use: [\n          {\n            loader: 'url-loader',\n            options: {\n              limit: 8192\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.svg$/,\n        exclude: [\n          path.join(process.cwd(), 'node_modules/grapesjs')\n        ],\n        use: [\n          {\n            loader: 'file-loader',\n            options: {\n              name: '[name].[ext]',\n              outputPath: 'svg/'\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.(graphql|gql)$/,\n        exclude: /node_modules/,\n        use: [\n          { loader: 'graphql-persisted-document-loader' },\n          { loader: 'graphql-tag/loader' }\n        ]\n      },\n      {\n        test: /\\.(woff2|woff|ttf|eot)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n        use: [{\n          loader: 'file-loader',\n          options: {\n            name: '[name].[ext]',\n            outputPath: 'fonts/'\n          }\n        }]\n      },\n      {\n        loader: 'webpack-modernizr-loader',\n        test: /\\.modernizrrc\\.js$/\n      }\n    ]\n  },\n  plugins: [\n    new VueLoaderPlugin(),\n    new VuetifyLoaderPlugin(),\n    new MomentTimezoneDataPlugin({\n      startYear: 2017,\n      endYear: (new Date().getFullYear()) + 5\n    }),\n    new CopyWebpackPlugin({\n      patterns: [\n        { from: 'client/static' },\n        { from: './node_modules/prismjs/components', to: 'js/prism' }\n      ]\n    }),\n    new HtmlWebpackPlugin({\n      template: 'dev/templates/master.pug',\n      filename: '../server/views/master.pug',\n      hash: false,\n      inject: false,\n      excludeChunks: ['setup', 'legacy']\n    }),\n    new HtmlWebpackPlugin({\n      template: 'dev/templates/legacy.pug',\n      filename: '../server/views/legacy/master.pug',\n      hash: false,\n      inject: false,\n      excludeChunks: ['setup', 'app']\n    }),\n    new HtmlWebpackPlugin({\n      template: 'dev/templates/setup.pug',\n      filename: '../server/views/setup.pug',\n      hash: false,\n      inject: false,\n      excludeChunks: ['app', 'legacy']\n    }),\n    new HtmlWebpackPugPlugin(),\n    new WebpackBarPlugin({\n      name: 'Client Assets'\n    }),\n    new webpack.DefinePlugin({\n      'process.env.NODE_ENV': JSON.stringify('development'),\n      'process.env.CURRENT_THEME': JSON.stringify(_.defaultTo(yargs.theme, 'default'))\n    }),\n    new WriteFilePlugin(),\n    new webpack.HotModuleReplacementPlugin(),\n    new webpack.WatchIgnorePlugin([\n      /node_modules/\n    ])\n  ],\n  optimization: {\n    namedModules: true,\n    namedChunks: true,\n    splitChunks: {\n      cacheGroups: {\n        default: {\n          minChunks: 2,\n          priority: -20,\n          reuseExistingChunk: true\n        },\n        vendor: {\n          test: /[\\\\/]node_modules[\\\\/]/,\n          minChunks: 2,\n          priority: -10\n        }\n      }\n    },\n    runtimeChunk: 'single'\n  },\n  resolve: {\n    mainFields: ['browser', 'main', 'module'],\n    symlinks: true,\n    alias: {\n      '@': path.join(process.cwd(), 'client'),\n      'vue$': 'vue/dist/vue.esm.js',\n      'gql': path.join(process.cwd(), 'client/graph'),\n      // Duplicates fixes:\n      'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'),\n      'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'),\n      'uc.micro': path.join(process.cwd(), 'node_modules/uc.micro'),\n      'modernizr$': path.resolve(process.cwd(), 'client/.modernizrrc.js')\n    },\n    extensions: [\n      '.js',\n      '.json',\n      '.vue'\n    ],\n    modules: [\n      'node_modules'\n    ]\n  },\n  node: {\n    fs: 'empty'\n  },\n  stats: {\n    children: false,\n    entrypoints: false\n  },\n  target: 'web',\n  watch: true\n}\n"
  },
  {
    "path": "dev/webpack/webpack.prod.js",
    "content": "const webpack = require('webpack')\nconst path = require('path')\nconst fs = require('fs-extra')\nconst yargs = require('yargs').argv\nconst _ = require('lodash')\n\nconst { VueLoaderPlugin } = require('vue-loader')\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst HtmlWebpackPugPlugin = require('html-webpack-pug-plugin')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin')\nconst OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')\nconst ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')\nconst VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')\nconst WebpackBarPlugin = require('webpackbar')\n\nconst now = Math.round(Date.now() / 1000)\n\nconst babelConfig = fs.readJsonSync(path.join(process.cwd(), '.babelrc'))\nconst cacheDir = '.webpack-cache/cache'\nconst babelDir = path.join(process.cwd(), '.webpack-cache/babel')\n\nprocess.noDeprecation = true\n\nfs.emptyDirSync(path.join(process.cwd(), 'assets'))\n\nmodule.exports = {\n  mode: 'production',\n  entry: {\n    app: './client/index-app.js',\n    legacy: './client/index-legacy.js',\n    setup: './client/index-setup.js'\n  },\n  output: {\n    path: path.join(process.cwd(), 'assets'),\n    publicPath: '/_assets/',\n    filename: `js/[name].js?${now}`,\n    chunkFilename: `js/[name].js?${now}`,\n    globalObject: 'this',\n    crossOriginLoading: 'use-credentials'\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: (modulePath) => {\n          return modulePath.includes('node_modules') && !modulePath.includes('vuetify')\n        },\n        use: [\n          {\n            loader: 'cache-loader',\n            options: {\n              cacheDirectory: cacheDir\n            }\n          },\n          {\n            loader: 'babel-loader',\n            options: {\n              ...babelConfig,\n              cacheDirectory: babelDir\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.css$/,\n        use: [\n          'style-loader',\n          MiniCssExtractPlugin.loader,\n          'css-loader',\n          'postcss-loader'\n        ]\n      },\n      {\n        test: /\\.sass$/,\n        use: [\n          {\n            loader: 'cache-loader',\n            options: {\n              cacheDirectory: cacheDir\n            }\n          },\n          'style-loader',\n          'css-loader',\n          'postcss-loader',\n          {\n            loader: 'sass-loader',\n            options: {\n              implementation: require('sass'),\n              sourceMap: false,\n              sassOptions: {\n                fiber: false\n              }\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n          {\n            loader: 'cache-loader',\n            options: {\n              cacheDirectory: cacheDir\n            }\n          },\n          'style-loader',\n          MiniCssExtractPlugin.loader,\n          'css-loader',\n          'postcss-loader',\n          {\n            loader: 'sass-loader',\n            options: {\n              implementation: require('sass'),\n              sourceMap: false,\n              sassOptions: {\n                fiber: false\n              }\n            }\n          },\n          {\n            loader: 'sass-resources-loader',\n            options: {\n              resources: path.join(process.cwd(), '/client/scss/global.scss')\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader'\n      },\n      {\n        test: /\\.pug$/,\n        exclude: [\n          path.join(process.cwd(), 'dev')\n        ],\n        loader: 'pug-plain-loader'\n      },\n      {\n        test: /\\.(png|jpg|gif)$/,\n        use: [\n          {\n            loader: 'url-loader',\n            options: {\n              limit: 8192\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.svg$/,\n        exclude: [\n          path.join(process.cwd(), 'node_modules/grapesjs')\n        ],\n        use: [\n          {\n            loader: 'file-loader',\n            options: {\n              name: '[name].[ext]',\n              outputPath: 'svg/'\n            }\n          }\n        ]\n      },\n      {\n        test: /\\.(graphql|gql)$/,\n        exclude: /node_modules/,\n        use: [\n          { loader: 'graphql-persisted-document-loader' },\n          { loader: 'graphql-tag/loader' }\n        ]\n      },\n      {\n        test: /\\.(woff2|woff|ttf|eot)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n        use: [{\n          loader: 'file-loader',\n          options: {\n            name: '[name].[ext]',\n            outputPath: 'fonts/'\n          }\n        }]\n      },\n      {\n        loader: 'webpack-modernizr-loader',\n        test: /\\.modernizrrc\\.js$/\n      }\n    ]\n  },\n  plugins: [\n    new VueLoaderPlugin(),\n    new VuetifyLoaderPlugin(),\n    new webpack.BannerPlugin('Wiki.js - wiki.js.org - Licensed under AGPL'),\n    new MomentTimezoneDataPlugin({\n      startYear: 2017,\n      endYear: (new Date().getFullYear()) + 5\n    }),\n    new CopyWebpackPlugin({\n      patterns: [\n        { from: 'client/static' },\n        { from: './node_modules/prismjs/components', to: 'js/prism' }\n      ]\n    }),\n    new MiniCssExtractPlugin({\n      filename: 'css/bundle.[hash].css',\n      chunkFilename: 'css/[name].[chunkhash].css'\n    }),\n    new HtmlWebpackPlugin({\n      template: 'dev/templates/master.pug',\n      filename: '../server/views/master.pug',\n      hash: false,\n      inject: false,\n      excludeChunks: ['setup', 'legacy']\n    }),\n    new HtmlWebpackPlugin({\n      template: 'dev/templates/legacy.pug',\n      filename: '../server/views/legacy/master.pug',\n      hash: false,\n      inject: false,\n      excludeChunks: ['setup', 'app']\n    }),\n    new HtmlWebpackPlugin({\n      template: 'dev/templates/setup.pug',\n      filename: '../server/views/setup.pug',\n      hash: false,\n      inject: false,\n      excludeChunks: ['app', 'legacy']\n    }),\n    new HtmlWebpackPugPlugin(),\n    new ScriptExtHtmlWebpackPlugin({\n      sync: 'runtime.js',\n      defaultAttribute: 'async'\n    }),\n    new WebpackBarPlugin({\n      name: 'Client Assets'\n    }),\n    new CleanWebpackPlugin(),\n    new OptimizeCssAssetsPlugin({\n      cssProcessorOptions: { discardComments: { removeAll: true } },\n      canPrint: true\n    }),\n    new webpack.DefinePlugin({\n      'process.env.NODE_ENV': JSON.stringify('production'),\n      'process.env.CURRENT_THEME': JSON.stringify(_.defaultTo(yargs.theme, 'default'))\n    }),\n    new webpack.optimize.MinChunkSizePlugin({\n      minChunkSize: 50000\n    })\n  ],\n  optimization: {\n    namedModules: true,\n    namedChunks: true,\n    splitChunks: {\n      name: 'vendor',\n      minChunks: 2\n    },\n    runtimeChunk: 'single'\n  },\n  resolve: {\n    mainFields: ['browser', 'main', 'module'],\n    symlinks: true,\n    alias: {\n      '@': path.join(process.cwd(), 'client'),\n      'vue$': 'vue/dist/vue.esm.js',\n      'gql': path.join(process.cwd(), 'client/graph'),\n      // Duplicates fixes:\n      'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'),\n      'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'),\n      'uc.micro': path.join(process.cwd(), 'node_modules/uc.micro'),\n      'modernizr$': path.resolve(process.cwd(), 'client/.modernizrrc.js')\n    },\n    extensions: [\n      '.js',\n      '.json',\n      '.vue'\n    ],\n    modules: [\n      'node_modules'\n    ]\n  },\n  node: {\n    fs: 'empty'\n  },\n  stats: {\n    children: false,\n    entrypoints: false\n  },\n  target: 'web'\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"wiki\",\n  \"version\": \"2.0.0\",\n  \"releaseDate\": \"2026-01-01T01:01:01.000Z\",\n  \"description\": \"A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown\",\n  \"main\": \"wiki.js\",\n  \"dev\": true,\n  \"scripts\": {\n    \"start\": \"node server\",\n    \"dev\": \"cross-env NODE_OPTIONS=--openssl-legacy-provider node dev\",\n    \"build\": \"cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js\",\n    \"watch\": \"cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config dev/webpack/webpack.dev.js\",\n    \"test\": \"eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest\",\n    \"cypress:open\": \"cypress open\",\n    \"postinstall\": \"patch-package\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Requarks/wiki.git\"\n  },\n  \"keywords\": [\n    \"wiki\",\n    \"wikis\",\n    \"docs\",\n    \"documentation\",\n    \"markdown\",\n    \"guides\",\n    \"knowledge base\"\n  ],\n  \"author\": \"Nicolas Giard\",\n  \"license\": \"AGPL-3.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Requarks/wiki/issues\"\n  },\n  \"homepage\": \"https://github.com/Requarks/wiki#readme\",\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"dependencies\": {\n    \"@azure/storage-blob\": \"12.29.1\",\n    \"@exlinc/keycloak-passport\": \"1.0.2\",\n    \"@joplin/turndown-plugin-gfm\": \"1.0.45\",\n    \"@root/csr\": \"0.8.1\",\n    \"@root/keypairs\": \"0.10.3\",\n    \"@root/pem\": \"1.0.4\",\n    \"acme\": \"3.0.3\",\n    \"akismet-api\": \"5.3.0\",\n    \"algoliasearch\": \"4.5.1\",\n    \"apollo-fetch\": \"0.7.0\",\n    \"apollo-server\": \"2.25.2\",\n    \"apollo-server-express\": \"2.25.2\",\n    \"asciidoctor\": \"2.2.6\",\n    \"auto-load\": \"3.0.4\",\n    \"aws-sdk\": \"2.1693.0\",\n    \"azure-search-client\": \"3.1.5\",\n    \"bcryptjs-then\": \"1.0.1\",\n    \"bluebird\": \"3.7.2\",\n    \"body-parser\": \"1.20.1\",\n    \"chalk\": \"4.1.0\",\n    \"cheerio\": \"1.0.0-rc.5\",\n    \"chokidar\": \"3.5.3\",\n    \"chromium-pickle-js\": \"0.2.0\",\n    \"clean-css\": \"5.3.3\",\n    \"command-exists\": \"1.2.9\",\n    \"compression\": \"1.8.1\",\n    \"connect-session-knex\": \"2.0.0\",\n    \"cookie-parser\": \"1.4.7\",\n    \"cors\": \"2.8.5\",\n    \"cuint\": \"0.2.2\",\n    \"custom-error-instance\": \"2.1.2\",\n    \"dependency-graph\": \"0.11.0\",\n    \"diff\": \"4.0.2\",\n    \"diff2html\": \"3.1.14\",\n    \"dompurify\": \"3.3.1\",\n    \"dotize\": \"0.3.0\",\n    \"elasticsearch6\": \"npm:@elastic/elasticsearch@6\",\n    \"elasticsearch7\": \"npm:@elastic/elasticsearch@7\",\n    \"elasticsearch8\": \"npm:@elastic/elasticsearch@8\",\n    \"emoji-regex\": \"10.2.1\",\n    \"eventemitter2\": \"6.4.9\",\n    \"express\": \"4.18.2\",\n    \"express-brute\": \"1.0.1\",\n    \"express-session\": \"1.18.2\",\n    \"file-type\": \"15.0.1\",\n    \"filesize\": \"6.1.0\",\n    \"fs-extra\": \"9.0.1\",\n    \"getos\": \"3.2.1\",\n    \"graphql\": \"15.3.0\",\n    \"graphql-list-fields\": \"2.0.2\",\n    \"graphql-rate-limit-directive\": \"1.2.1\",\n    \"graphql-subscriptions\": \"1.1.0\",\n    \"graphql-tools\": \"7.0.0\",\n    \"he\": \"1.2.0\",\n    \"highlight.js\": \"10.3.1\",\n    \"i18next\": \"19.8.3\",\n    \"i18next-express-middleware\": \"2.0.0\",\n    \"i18next-node-fs-backend\": \"2.1.3\",\n    \"image-size\": \"0.9.2\",\n    \"js-base64\": \"3.7.8\",\n    \"js-binary\": \"1.2.0\",\n    \"js-yaml\": \"3.14.0\",\n    \"jsdom\": \"16.4.0\",\n    \"jsonwebtoken\": \"9.0.3\",\n    \"katex\": \"0.12.0\",\n    \"klaw\": \"3.0.0\",\n    \"knex\": \"0.21.7\",\n    \"lodash\": \"4.17.21\",\n    \"luxon\": \"1.25.0\",\n    \"markdown-it\": \"11.0.1\",\n    \"markdown-it-abbr\": \"1.0.4\",\n    \"markdown-it-attrs\": \"3.0.3\",\n    \"markdown-it-decorate\": \"1.2.2\",\n    \"markdown-it-emoji\": \"3.0.0\",\n    \"markdown-it-expand-tabs\": \"1.0.13\",\n    \"markdown-it-external-links\": \"0.0.6\",\n    \"markdown-it-footnote\": \"3.0.3\",\n    \"markdown-it-imsize\": \"2.0.1\",\n    \"markdown-it-mark\": \"3.0.1\",\n    \"markdown-it-mathjax\": \"2.0.0\",\n    \"markdown-it-multimd-table\": \"4.0.3\",\n    \"markdown-it-pivot-table\": \"1.0.5\",\n    \"markdown-it-sub\": \"1.0.0\",\n    \"markdown-it-sup\": \"1.0.0\",\n    \"markdown-it-task-lists\": \"2.1.1\",\n    \"mathjax\": \"3.2.2\",\n    \"mime-types\": \"2.1.35\",\n    \"moment\": \"2.30.1\",\n    \"moment-timezone\": \"0.6.0\",\n    \"mongodb\": \"3.6.5\",\n    \"ms\": \"2.1.3\",\n    \"mssql\": \"6.2.3\",\n    \"multer\": \"1.4.4\",\n    \"mysql2\": \"3.16.0\",\n    \"nanoid\": \"3.2.0\",\n    \"node-2fa\": \"1.1.2\",\n    \"node-cache\": \"5.1.2\",\n    \"nodemailer\": \"6.9.1\",\n    \"objection\": \"2.2.18\",\n    \"passport\": \"0.4.1\",\n    \"passport-auth0\": \"1.4.5\",\n    \"passport-azure-ad\": \"4.3.5\",\n    \"passport-cas\": \"0.1.1\",\n    \"passport-discord\": \"0.1.4\",\n    \"passport-dropbox-oauth2\": \"1.1.0\",\n    \"passport-facebook\": \"3.0.0\",\n    \"passport-github2\": \"0.1.12\",\n    \"passport-gitlab2\": \"5.0.0\",\n    \"passport-google-oauth20\": \"2.0.0\",\n    \"passport-jwt\": \"4.0.1\",\n    \"passport-ldapauth\": \"3.0.1\",\n    \"passport-local\": \"1.0.0\",\n    \"passport-microsoft\": \"0.1.0\",\n    \"passport-oauth2\": \"1.8.0\",\n    \"passport-okta-oauth\": \"0.0.1\",\n    \"passport-openidconnect\": \"0.1.2\",\n    \"passport-saml\": \"3.2.4\",\n    \"passport-slack-oauth2\": \"1.2.0\",\n    \"passport-twitch-strategy\": \"2.2.0\",\n    \"patch-package\": \"8.0.1\",\n    \"pem-jwk\": \"2.0.0\",\n    \"pg\": \"8.16.3\",\n    \"pg-hstore\": \"2.3.4\",\n    \"pg-pubsub\": \"0.8.1\",\n    \"pg-query-stream\": \"4.10.3\",\n    \"pg-tsquery\": \"8.4.2\",\n    \"postinstall-postinstall\": \"2.1.0\",\n    \"pug\": \"3.0.3\",\n    \"punycode\": \"2.3.1\",\n    \"qr-image\": \"3.2.0\",\n    \"raven\": \"2.6.4\",\n    \"remove-markdown\": \"0.6.2\",\n    \"request\": \"2.88.2\",\n    \"request-promise\": \"4.2.6\",\n    \"safe-regex\": \"2.1.1\",\n    \"sanitize-filename\": \"1.6.3\",\n    \"scim-query-filter-parser\": \"2.0.4\",\n    \"semver\": \"7.7.3\",\n    \"serve-favicon\": \"2.5.1\",\n    \"simple-git\": \"3.30.0\",\n    \"solr-node\": \"1.2.1\",\n    \"sqlite3\": \"5.1.7\",\n    \"ssh2\": \"1.11.0\",\n    \"ssh2-promise\": \"1.0.3\",\n    \"striptags\": \"3.2.0\",\n    \"subscriptions-transport-ws\": \"0.9.18\",\n    \"tar-fs\": \"2.1.1\",\n    \"turndown\": \"7.2.2\",\n    \"twemoji\": \"14.0.2\",\n    \"uslug\": \"1.0.4\",\n    \"uuid\": \"9.0.0\",\n    \"validate.js\": \"0.13.1\",\n    \"winston\": \"3.8.2\",\n    \"xss\": \"1.0.15\",\n    \"yargs\": \"17.6.2\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"^7.12.1\",\n    \"@babel/core\": \"^7.12.3\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.12.1\",\n    \"@babel/plugin-proposal-decorators\": \"^7.12.1\",\n    \"@babel/plugin-proposal-export-namespace-from\": \"^7.12.1\",\n    \"@babel/plugin-proposal-function-sent\": \"^7.12.1\",\n    \"@babel/plugin-proposal-json-strings\": \"^7.12.1\",\n    \"@babel/plugin-proposal-numeric-separator\": \"^7.12.1\",\n    \"@babel/plugin-proposal-throw-expressions\": \"^7.12.1\",\n    \"@babel/plugin-syntax-dynamic-import\": \"^7.8.3\",\n    \"@babel/plugin-syntax-import-meta\": \"^7.10.4\",\n    \"@babel/polyfill\": \"^7.12.1\",\n    \"@babel/preset-env\": \"^7.12.1\",\n    \"@mdi/font\": \"5.8.55\",\n    \"@panter/vue-i18next\": \"0.15.2\",\n    \"@requarks/ckeditor5\": \"19.0.1-wiki.2\",\n    \"@vue/babel-preset-app\": \"4.5.15\",\n    \"animate-sass\": \"0.8.2\",\n    \"animated-number-vue\": \"1.0.0\",\n    \"apollo-cache-inmemory\": \"1.6.6\",\n    \"apollo-client\": \"2.6.10\",\n    \"apollo-link\": \"1.2.14\",\n    \"apollo-link-batch-http\": \"1.2.14\",\n    \"apollo-link-error\": \"1.1.13\",\n    \"apollo-link-http\": \"1.5.17\",\n    \"apollo-link-persisted-queries\": \"0.2.5\",\n    \"apollo-link-ws\": \"1.0.20\",\n    \"apollo-utilities\": \"1.3.4\",\n    \"autoprefixer\": \"9.8.6\",\n    \"babel-eslint\": \"10.1.0\",\n    \"babel-jest\": \"26.6.1\",\n    \"babel-loader\": \"^8.1.0\",\n    \"babel-plugin-graphql-tag\": \"3.1.0\",\n    \"babel-plugin-lodash\": \"3.3.4\",\n    \"babel-plugin-prismjs\": \"2.0.1\",\n    \"babel-plugin-transform-imports\": \"2.0.0\",\n    \"cache-loader\": \"4.1.0\",\n    \"canvas-confetti\": \"1.3.1\",\n    \"cash-dom\": \"8.1.3\",\n    \"chart.js\": \"2.9.4\",\n    \"clean-webpack-plugin\": \"3.0.0\",\n    \"clipboard\": \"2.0.11\",\n    \"codemirror\": \"5.58.2\",\n    \"codemirror-asciidoc\": \"1.0.4\",\n    \"copy-webpack-plugin\": \"6.2.1\",\n    \"core-js\": \"3.6.5\",\n    \"cross-env\": \"10.0.0\",\n    \"css-loader\": \"4.3.0\",\n    \"cssnano\": \"4.1.10\",\n    \"cypress\": \"5.3.0\",\n    \"d3\": \"6.2.0\",\n    \"duplicate-package-checker-webpack-plugin\": \"3.0.0\",\n    \"epic-spinners\": \"1.1.0\",\n    \"eslint\": \"7.12.0\",\n    \"eslint-config-requarks\": \"1.0.7\",\n    \"eslint-config-standard\": \"15.0.0\",\n    \"eslint-plugin-cypress\": \"2.11.2\",\n    \"eslint-plugin-import\": \"2.22.1\",\n    \"eslint-plugin-node\": \"11.1.0\",\n    \"eslint-plugin-promise\": \"4.2.1\",\n    \"eslint-plugin-standard\": \"4.0.2\",\n    \"eslint-plugin-vue\": \"7.1.0\",\n    \"file-loader\": \"6.1.1\",\n    \"filepond\": \"4.21.1\",\n    \"filepond-plugin-file-validate-type\": \"1.2.8\",\n    \"filesize.js\": \"2.0.0\",\n    \"graphql-persisted-document-loader\": \"2.0.0\",\n    \"graphql-tag\": \"2.11.0\",\n    \"hammerjs\": \"2.0.8\",\n    \"html-webpack-plugin\": \"4.5.0\",\n    \"html-webpack-pug-plugin\": \"2.0.0\",\n    \"i18next-chained-backend\": \"2.0.1\",\n    \"i18next-localstorage-backend\": \"3.1.3\",\n    \"i18next-xhr-backend\": \"3.2.2\",\n    \"ignore-loader\": \"0.1.2\",\n    \"jest\": \"26.6.1\",\n    \"js-beautify\": \"1.13.5\",\n    \"js-cookie\": \"2.2.1\",\n    \"mermaid\": \"8.8.2\",\n    \"mini-css-extract-plugin\": \"0.11.3\",\n    \"moment-duration-format\": \"2.3.2\",\n    \"moment-timezone-data-webpack-plugin\": \"1.3.0\",\n    \"offline-plugin\": \"5.0.7\",\n    \"optimize-css-assets-webpack-plugin\": \"5.0.4\",\n    \"pako\": \"1.0.11\",\n    \"postcss-cssnext\": \"3.1.1\",\n    \"postcss-flexbugs-fixes\": \"4.2.1\",\n    \"postcss-flexibility\": \"2.0.0\",\n    \"postcss-import\": \"12.0.1\",\n    \"postcss-loader\": \"3.0.0\",\n    \"postcss-preset-env\": \"6.7.0\",\n    \"postcss-selector-parser\": \"6.0.11\",\n    \"prismjs\": \"1.22.0\",\n    \"pug-lint\": \"2.6.0\",\n    \"pug-loader\": \"2.4.0\",\n    \"pug-plain-loader\": \"1.0.0\",\n    \"raw-loader\": \"4.0.2\",\n    \"resolve-url-loader\": \"3.1.2\",\n    \"sass\": \"1.27.0\",\n    \"sass-loader\": \"10.0.4\",\n    \"sass-resources-loader\": \"2.1.1\",\n    \"script-ext-html-webpack-plugin\": \"2.1.5\",\n    \"simple-progress-webpack-plugin\": \"1.1.2\",\n    \"style-loader\": \"1.3.0\",\n    \"terser\": \"5.3.8\",\n    \"twemoji-awesome\": \"1.0.6\",\n    \"url-loader\": \"4.1.1\",\n    \"velocity-animate\": \"1.5.2\",\n    \"viz.js\": \"2.1.2\",\n    \"vue\": \"2.6.14\",\n    \"vue-apollo\": \"3.0.5\",\n    \"vue-chartjs\": \"3.5.1\",\n    \"vue-clipboards\": \"1.3.0\",\n    \"vue-filepond\": \"6.0.3\",\n    \"vue-hot-reload-api\": \"2.3.4\",\n    \"vue-loader\": \"15.9.8\",\n    \"vue-moment\": \"4.1.0\",\n    \"vue-router\": \"3.4.7\",\n    \"vue-status-indicator\": \"1.2.1\",\n    \"vue-template-compiler\": \"2.6.14\",\n    \"vue2-animate\": \"2.1.4\",\n    \"vuedraggable\": \"2.24.3\",\n    \"vuescroll\": \"4.16.1\",\n    \"vuetify\": \"2.3.15\",\n    \"vuetify-loader\": \"1.6.0\",\n    \"vuex\": \"3.5.1\",\n    \"vuex-pathify\": \"1.4.5\",\n    \"vuex-persistedstate\": \"3.1.0\",\n    \"webpack\": \"4.44.2\",\n    \"webpack-bundle-analyzer\": \"3.9.0\",\n    \"webpack-cli\": \"3.3.12\",\n    \"webpack-dev-middleware\": \"3.7.2\",\n    \"webpack-hot-middleware\": \"2.25.3\",\n    \"webpack-merge\": \"5.2.0\",\n    \"webpack-modernizr-loader\": \"5.0.0\",\n    \"webpack-subresource-integrity\": \"1.5.1\",\n    \"webpackbar\": \"4.0.0\",\n    \"whatwg-fetch\": \"3.6.2\",\n    \"write-file-webpack-plugin\": \"4.5.1\",\n    \"xterm\": \"4.9.0\",\n    \"zxcvbn\": \"4.4.2\"\n  },\n  \"resolutions\": {\n    \"apollo-server-express/**/graphql-tools\": \"4.0.8\",\n    \"graphql\": \"15.3.0\",\n    \"passport-saml/**/xml-crypto\": \"2.1.6\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 major versions\",\n    \"Firefox ESR\",\n    \"not ie > 0\",\n    \"not ie_mob > 0\",\n    \"not android > 0\",\n    \"not dead\"\n  ],\n  \"postcss\": {\n    \"plugins\": {\n      \"autoprefixer\": {},\n      \"cssnano\": {\n        \"preset\": [\n          \"default\",\n          {\n            \"discardComments\": {\n              \"removeAll\": true\n            }\n          }\n        ]\n      },\n      \"postcss-flexbugs-fixes\": {},\n      \"postcss-flexibility\": {}\n    }\n  },\n  \"pugLintConfig\": {\n    \"disallowDuplicateAttributes\": true,\n    \"disallowIdAttributeWithStaticValue\": true,\n    \"disallowMultipleLineBreaks\": true,\n    \"requireClassLiteralsBeforeAttributes\": true,\n    \"requireIdLiteralsBeforeAttributes\": true,\n    \"requireLineFeedAtFileEnd\": true,\n    \"requireLowerCaseAttributes\": true,\n    \"requireLowerCaseTags\": true,\n    \"requireSpaceAfterCodeOperator\": true,\n    \"requireStrictEqualityOperators\": true,\n    \"validateAttributeQuoteMarks\": \"'\",\n    \"validateAttributeSeparator\": {\n      \"separator\": \", \",\n      \"multiLineSeparator\": \"\\n  \"\n    },\n    \"validateDivTags\": true,\n    \"validateIndentation\": 2,\n    \"excludeFiles\": [\n      \"node_modules/**\",\n      \"server/views/master.pug\",\n      \"server/views/setup.pug\",\n      \"server/views/legacy/master.pug\"\n    ]\n  },\n  \"collective\": {\n    \"type\": \"opencollective\",\n    \"url\": \"https://opencollective.com/wikijs\",\n    \"logo\": \"https://opencollective.com/opencollective/logo.txt\"\n  }\n}\n"
  },
  {
    "path": "patches/extract-files+9.0.0.patch",
    "content": "diff --git a/node_modules/extract-files/package.json b/node_modules/extract-files/package.json\nindex 636fa03..1b75f79 100644\n--- a/node_modules/extract-files/package.json\n+++ b/node_modules/extract-files/package.json\n@@ -34,6 +34,9 @@\n       \"import\": \"./public/index.mjs\",\n       \"require\": \"./public/index.js\"\n     },\n+    \"./public/extractFiles\": \"./public/extractFiles.js\",\n+    \"./public/isExtractableFile\": \"./public/isExtractableFile.js\",\n+    \"./public/ReactNativeFile\": \"./public/ReactNativeFile.js\",\n     \"./public/\": \"./public/\",\n     \"./package\": \"./package.json\",\n     \"./package.json\": \"./package.json\"\n"
  },
  {
    "path": "server/app/content/create.md",
    "content": "<!-- TITLE: {TITLE} -->\n<!-- SUBTITLE: A quick summary of {TITLE} -->\n\n# Header"
  },
  {
    "path": "server/app/data.yml",
    "content": "# ---------------------------------\n# DO NOT EDIT THIS FILE!\n# This is reserved for system use!\n# ---------------------------------\nname: Wiki.js\ndefaults:\n  config:\n    # File defaults\n    port: 80\n    db:\n      type: postgres\n      host: localhost\n      port: 5432\n      user: wikijs\n      pass: wikijsrocks\n      db: wiki\n      ssl: false\n      storage: ./db.sqlite\n      sslOptions:\n        auto: true\n    ssl:\n      enabled: false\n    pool:\n      min: 1\n    bindIP: 0.0.0.0\n    logLevel: info\n    logFormat: default\n    offline: false\n    ha: false\n    bodyParserLimit: 5mb\n    # DB defaults\n    api:\n      isEnabled: false\n    graphEndpoint: 'https://graph.requarks.io'\n    lang:\n      code: en\n      autoUpdate: true\n      namespaces: []\n      namespacing: false\n      rtl: false\n    telemetry:\n      clientId: ''\n      isEnabled: false\n    title: Wiki.js\n    company: ''\n    contentLicense: ''\n    footerOverride: ''\n    logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg\n    pageExtensions:\n      - md\n      - html\n      - txt\n    mail:\n      host: ''\n      secure: true\n      verifySSL: true\n    nav:\n      mode: 'MIXED'\n    theming:\n      theme: 'default'\n      iconset: 'md'\n      darkMode: false\n      tocPosition: 'left'\n    auth:\n      autoLogin: false\n      enforce2FA: false\n      hideLocal: false\n      loginBgUrl: ''\n      audience: 'urn:wiki.js'\n      tokenExpiration: '30m'\n      tokenRenewal: '14d'\n    editShortcuts:\n      editFab: true\n      editMenuBar: false\n      editMenuBtn: true\n      editMenuExternalBtn: true\n      editMenuExternalName: 'GitHub'\n      editMenuExternalIcon: 'mdi-github'\n      editMenuExternalUrl: 'https://github.com/org/repo/blob/main/{filename}'\n    features:\n      featurePageRatings: true\n      featurePageComments: true\n      featurePersonalWikis: true\n    security:\n      securityOpenRedirect: true\n      securityIframe: true\n      securityReferrerPolicy: true\n      securityTrustProxy: false\n      securitySRI: true\n      securityHSTS: false\n      securityHSTSDuration: 300\n      securityCSP: false\n      securityCSPDirectives: ''\n    server:\n      sslRedir: false\n    uploads:\n      maxFileSize: 5242880\n      maxFiles: 10\n      scanSVG: true\n      forceDownload: true\n    flags:\n      ldapdebug: false\n      sqllog: false\n    # System defaults\n    channel: STABLE\n    setup: false\n    dataPath: ./data\n    cors:\n      credentials: true\n      maxAge: 600\n      methods: 'GET,POST'\n      origin: true\n    search:\n      maxHits: 100\n    maintainerEmail: security@requarks.io\nlocaleNamespaces:\n  - admin\n  - auth\n  - common\njobs:\n  purgeUploads:\n    onInit: true\n    schedule: PT15M\n    offlineSkip: false\n    repeat: true\n  syncGraphLocales:\n    onInit: true\n    schedule: P1D\n    offlineSkip: true\n    repeat: true\n  syncGraphUpdates:\n    onInit: true\n    schedule: P1D\n    offlineSkip: true\n    repeat: true\n  rebuildTree:\n    onInit: true\n    offlineSkip: false\n    repeat: false\n    immediate: true\n    worker: true\ngroups:\n  defaultPermissions:\n    - 'read:pages'\n    - 'read:assets'\n    - 'read:comments'\n    - 'write:comments'\n  defaultPageRules:\n    - id: default\n      deny: false\n      match: START\n      roles:\n        - 'read:pages'\n        - 'read:assets'\n        - 'read:comments'\n        - 'write:comments'\n      path: ''\n      locales: []\nreservedPaths:\n  - login\n  - logout\n  - register\n  - verify\n  - favicons\n  - fonts\n  - img\n  - js\n  - svg\n# ---------------------------------\n"
  },
  {
    "path": "server/app/regex.js",
    "content": "'use strict'\n\nmodule.exports = {\n  arabic: '\\u0600-\\u06ff\\u0750-\\u077f\\ufb50-\\ufc3f\\ufe70-\\ufefc',\n  cjk: '\\u4E00-\\u9FBF\\u3040-\\u309F\\u30A0-\\u30FFㄱ-ㅎ가-힣ㅏ-ㅣ',\n  youtube: /(?:(?:youtu\\.be\\/|v\\/|vi\\/|u\\/\\w\\/|embed\\/)|(?:(?:watch)?\\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/,\n  vimeo: /vimeo.com\\/(?:channels\\/(?:\\w+\\/)?|groups\\/(?:[^/]*)\\/videos\\/|album\\/(?:\\d+)\\/video\\/|)(\\d+)(?:$|\\/|\\?)/,\n  dailymotion: /(?:dailymotion\\.com(?:\\/embed)?(?:\\/video|\\/hub)|dai\\.ly)\\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/\n}\n"
  },
  {
    "path": "server/controllers/auth.js",
    "content": "/* global WIKI */\n\nconst express = require('express')\nconst ExpressBrute = require('express-brute')\nconst BruteKnex = require('../helpers/brute-knex')\nconst router = express.Router()\nconst _ = require('lodash')\nconst commonHelper = require('../helpers/common')\n\nconst bruteforce = new ExpressBrute(new BruteKnex({\n  createTable: true,\n  knex: WIKI.models.knex\n}), {\n  freeRetries: 5,\n  minWait: 5 * 60 * 1000, // 5 minutes\n  maxWait: 60 * 60 * 1000, // 1 hour\n  failCallback: (req, res, next) => {\n    res.status(401).send('Too many failed attempts. Try again later.')\n  }\n})\n\n/**\n * Login form\n */\nrouter.get('/login', async (req, res, next) => {\n  _.set(res.locals, 'pageMeta.title', 'Login')\n\n  if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) {\n    const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient()\n    res.render('legacy/login', {\n      err: false,\n      formStrategies,\n      socialStrategies\n    })\n  } else {\n    // -> Bypass Login\n    if (WIKI.config.auth.autoLogin && !req.query.all) {\n      const stg = await WIKI.models.authentication.query().orderBy('order').first()\n      const stgInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey])\n      if (!stgInfo.useForm) {\n        return res.redirect(`/login/${stg.key}`)\n      }\n    }\n    // -> Show Login\n    const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'\n    res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal })\n  }\n})\n\n/**\n * Social Strategies Login\n */\nrouter.get('/login/:strategy', async (req, res, next) => {\n  try {\n    await WIKI.models.users.login({\n      strategy: req.params.strategy\n    }, { req, res })\n  } catch (err) {\n    next(err)\n  }\n})\n\n/**\n * Social Strategies Callback\n */\nrouter.all('/login/:strategy/callback', async (req, res, next) => {\n  if (req.method !== 'GET' && req.method !== 'POST') { return next() }\n\n  try {\n    const authResult = await WIKI.models.users.login({\n      strategy: req.params.strategy\n    }, { req, res })\n    res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts())\n\n    const loginRedirect = req.cookies['loginRedirect']\n    const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://')\n    if (loginRedirect === '/' && authResult.redirect) {\n      res.clearCookie('loginRedirect')\n      res.redirect(authResult.redirect)\n    } else if (isValidRedirect) {\n      res.clearCookie('loginRedirect')\n      res.redirect(loginRedirect)\n    } else {\n      if (loginRedirect) {\n        res.clearCookie('loginRedirect')\n      }\n      if (authResult.redirect) {\n        res.redirect(authResult.redirect)\n      } else {\n        res.redirect('/')\n      }\n    }\n  } catch (err) {\n    next(err)\n  }\n})\n\n/**\n * LEGACY - Login form handling\n */\nrouter.post('/login', bruteforce.prevent, async (req, res, next) => {\n  _.set(res.locals, 'pageMeta.title', 'Login')\n  if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) {\n    try {\n      const authResult = await WIKI.models.users.login({\n        strategy: req.body.strategy,\n        username: req.body.user,\n        password: req.body.pass\n      }, { req, res })\n      req.brute.reset()\n      res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts())\n      res.redirect('/')\n    } catch (err) {\n      const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient()\n      res.render('legacy/login', {\n        err,\n        formStrategies,\n        socialStrategies\n      })\n    }\n  } else {\n    res.redirect('/login')\n  }\n})\n\n/**\n * Logout\n */\nrouter.get('/logout', async (req, res) => {\n  const redirURL = await WIKI.models.users.logout({ req, res })\n  req.logout()\n  res.clearCookie('jwt')\n  res.redirect(redirURL)\n})\n\n/**\n * Register form\n */\nrouter.get('/register', async (req, res, next) => {\n  _.set(res.locals, 'pageMeta.title', 'Register')\n  const localStrg = await WIKI.models.authentication.getStrategy('local')\n  if (localStrg.selfRegistration) {\n    res.render('register')\n  } else {\n    next(new WIKI.Error.AuthRegistrationDisabled())\n  }\n})\n\n/**\n * Verify\n */\nrouter.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {\n  try {\n    const usr = await WIKI.models.userKeys.validateToken({ kind: 'verify', token: req.params.token })\n    await WIKI.models.users.query().patch({ isVerified: true }).where('id', usr.id)\n    req.brute.reset()\n    if (WIKI.config.auth.enforce2FA) {\n      res.redirect('/login')\n    } else {\n      const result = await WIKI.models.users.refreshToken(usr)\n      res.cookie('jwt', result.token, commonHelper.getCookieOpts())\n      res.redirect('/')\n    }\n  } catch (err) {\n    next(err)\n  }\n})\n\n/**\n * Reset Password\n */\nrouter.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => {\n  try {\n    const usr = await WIKI.models.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token })\n    if (!usr) {\n      throw new Error('Invalid Token')\n    }\n    req.brute.reset()\n\n    const changePwdContinuationToken = await WIKI.models.userKeys.generateToken({\n      userId: usr.id,\n      kind: 'changePwd'\n    })\n    const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'\n    res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken })\n  } catch (err) {\n    next(err)\n  }\n})\n\n/**\n * JWT Public Endpoints\n */\nrouter.get('/.well-known/jwk.json', function (req, res, next) {\n  res.json(WIKI.config.certs.jwk)\n})\nrouter.get('/.well-known/jwk.pem', function (req, res, next) {\n  res.send(WIKI.config.certs.public)\n})\n\nmodule.exports = router\n"
  },
  {
    "path": "server/controllers/common.js",
    "content": "const express = require('express')\nconst router = express.Router()\nconst pageHelper = require('../helpers/page')\nconst _ = require('lodash')\nconst CleanCSS = require('clean-css')\nconst moment = require('moment')\nconst qs = require('querystring')\n\n/* global WIKI */\n\nconst tmplCreateRegex = /^[0-9]+(,[0-9]+)?$/\n\n/**\n * Robots.txt\n */\nrouter.get('/robots.txt', (req, res, next) => {\n  res.type('text/plain')\n  if (_.includes(WIKI.config.seo.robots, 'noindex')) {\n    res.send('User-agent: *\\nDisallow: /')\n  } else {\n    res.status(200).end()\n  }\n})\n\n/**\n * Health Endpoint\n */\nrouter.get('/healthz', (req, res, next) => {\n  if (WIKI.models.knex.client.pool.numFree() < 1 && WIKI.models.knex.client.pool.numUsed() < 1) {\n    res.status(503).json({ ok: false }).end()\n  } else {\n    res.status(200).json({ ok: true }).end()\n  }\n})\n\n/**\n * Administration\n */\nrouter.get(['/a', '/a/*'], (req, res, next) => {\n  if (!WIKI.auth.checkAccess(req.user, [\n    'manage:system',\n    'write:users',\n    'manage:users',\n    'write:groups',\n    'manage:groups',\n    'manage:navigation',\n    'manage:theme',\n    'manage:api'\n  ])) {\n    _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n    return res.status(403).render('unauthorized', { action: 'view' })\n  }\n\n  _.set(res.locals, 'pageMeta.title', 'Admin')\n  res.render('admin')\n})\n\n/**\n * Download Page / Version\n */\nrouter.get(['/d', '/d/*'], async (req, res, next) => {\n  const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })\n\n  const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0\n\n  const page = await WIKI.models.pages.getPageFromDb({\n    path: pageArgs.path,\n    locale: pageArgs.locale,\n    userId: req.user.id,\n    isPrivate: false\n  })\n\n  pageArgs.tags = _.get(page, 'tags', [])\n\n  if (versionId > 0) {\n    if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {\n      _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n      return res.status(403).render('unauthorized', { action: 'downloadVersion' })\n    }\n  } else {\n    if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {\n      _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n      return res.status(403).render('unauthorized', { action: 'download' })\n    }\n  }\n\n  if (page) {\n    const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)\n    res.attachment(fileName)\n    if (versionId > 0) {\n      const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId })\n      res.send(pageHelper.injectPageMetadata(pageVersion))\n    } else {\n      res.send(pageHelper.injectPageMetadata(page))\n    }\n  } else {\n    res.status(404).end()\n  }\n})\n\n/**\n * Create/Edit document\n */\nrouter.get(['/e', '/e/*'], async (req, res, next) => {\n  const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })\n\n  if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {\n    return res.redirect(`/e/${pageArgs.locale}/${pageArgs.path}`)\n  }\n\n  req.i18n.changeLanguage(pageArgs.locale)\n\n  // -> Set Editor Lang\n  _.set(res, 'locals.siteConfig.lang', pageArgs.locale)\n  _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')\n\n  // -> Check for reserved path\n  if (pageHelper.isReservedPath(pageArgs.path)) {\n    return next(new Error('Cannot create this page because it starts with a system reserved path.'))\n  }\n\n  // -> Get page data from DB\n  let page = await WIKI.models.pages.getPageFromDb({\n    path: pageArgs.path,\n    locale: pageArgs.locale,\n    userId: req.user.id,\n    isPrivate: false\n  })\n\n  pageArgs.tags = _.get(page, 'tags', [])\n\n  // -> Effective Permissions\n  const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)\n\n  const injectCode = {\n    css: WIKI.config.theming.injectCSS,\n    head: WIKI.config.theming.injectHead,\n    body: WIKI.config.theming.injectBody\n  }\n\n  if (page) {\n    // -> EDIT MODE\n    if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {\n      _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n      return res.status(403).render('unauthorized', { action: 'edit' })\n    }\n\n    // -> Get page tags\n    await page.$relatedQuery('tags')\n    page.tags = _.map(page.tags, 'tag')\n\n    // Handle missing extra field\n    page.extra = page.extra || { css: '', js: '' }\n\n    // -> Beautify Script CSS\n    if (!_.isEmpty(page.extra.css)) {\n      page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles\n    }\n\n    _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)\n    _.set(res.locals, 'pageMeta.description', page.description)\n    page.mode = 'update'\n    page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'\n    page.content = Buffer.from(page.content).toString('base64')\n  } else {\n    // -> CREATE MODE\n    if (!effectivePermissions.pages.write) {\n      _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n      return res.status(403).render('unauthorized', { action: 'create' })\n    }\n\n    _.set(res.locals, 'pageMeta.title', `New Page`)\n    page = {\n      path: pageArgs.path,\n      localeCode: pageArgs.locale,\n      editorKey: null,\n      mode: 'create',\n      content: null,\n      title: null,\n      description: null,\n      updatedAt: new Date().toISOString(),\n      extra: {\n        css: '',\n        js: ''\n      }\n    }\n\n    // -> From Template\n    if (req.query.from && tmplCreateRegex.test(req.query.from)) {\n      let tmplPageId = 0\n      let tmplVersionId = 0\n      if (req.query.from.indexOf(',')) {\n        const q = req.query.from.split(',')\n        tmplPageId = _.toSafeInteger(q[0])\n        tmplVersionId = _.toSafeInteger(q[1])\n      } else {\n        tmplPageId = _.toSafeInteger(req.query.from)\n      }\n\n      if (tmplVersionId > 0) {\n        // -> From Page Version\n        const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: tmplPageId, versionId: tmplVersionId })\n        if (!pageVersion) {\n          _.set(res.locals, 'pageMeta.title', 'Page Not Found')\n          return res.status(404).render('notfound', { action: 'template' })\n        }\n        if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) {\n          _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n          return res.status(403).render('unauthorized', { action: 'sourceVersion' })\n        }\n        page.content = Buffer.from(pageVersion.content).toString('base64')\n        page.editorKey = pageVersion.editor\n        page.title = pageVersion.title\n        page.description = pageVersion.description\n      } else {\n        // -> From Page Live\n        const pageOriginal = await WIKI.models.pages.query().findById(tmplPageId)\n        if (!pageOriginal) {\n          _.set(res.locals, 'pageMeta.title', 'Page Not Found')\n          return res.status(404).render('notfound', { action: 'template' })\n        }\n        if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) {\n          _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n          return res.status(403).render('unauthorized', { action: 'source' })\n        }\n        page.content = Buffer.from(pageOriginal.content).toString('base64')\n        page.editorKey = pageOriginal.editorKey\n        page.title = pageOriginal.title\n        page.description = pageOriginal.description\n      }\n    }\n  }\n\n  res.render('editor', { page, injectCode, effectivePermissions })\n})\n\n/**\n * History\n */\nrouter.get(['/h', '/h/*'], async (req, res, next) => {\n  const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })\n\n  if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {\n    return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)\n  }\n\n  req.i18n.changeLanguage(pageArgs.locale)\n\n  _.set(res, 'locals.siteConfig.lang', pageArgs.locale)\n  _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')\n\n  const page = await WIKI.models.pages.getPageFromDb({\n    path: pageArgs.path,\n    locale: pageArgs.locale,\n    userId: req.user.id,\n    isPrivate: false\n  })\n\n  if (!page) {\n    _.set(res.locals, 'pageMeta.title', 'Page Not Found')\n    return res.status(404).render('notfound', { action: 'history' })\n  }\n\n  pageArgs.tags = _.get(page, 'tags', [])\n\n  const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)\n\n  if (!effectivePermissions.history.read) {\n    _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n    return res.render('unauthorized', { action: 'history' })\n  }\n\n  if (page) {\n    _.set(res.locals, 'pageMeta.title', page.title)\n    _.set(res.locals, 'pageMeta.description', page.description)\n\n    res.render('history', { page, effectivePermissions })\n  } else {\n    res.redirect(`/${pageArgs.path}`)\n  }\n})\n\n/**\n * Page ID redirection\n */\nrouter.get(['/i', '/i/:id'], async (req, res, next) => {\n  const pageId = _.toSafeInteger(req.params.id)\n  if (pageId <= 0) {\n    return res.redirect('/')\n  }\n\n  const page = await WIKI.models.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)\n  if (!page) {\n    _.set(res.locals, 'pageMeta.title', 'Page Not Found')\n    return res.status(404).render('notfound', { action: 'view' })\n  }\n\n  if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {\n    locale: page.localeCode,\n    path: page.path,\n    private: page.isPrivate,\n    privateNS: page.privateNS,\n    explicitLocale: false,\n    tags: page.tags\n  })) {\n    _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n    return res.status(403).render('unauthorized', { action: 'view' })\n  }\n\n  if (WIKI.config.lang.namespacing) {\n    return res.redirect(`/${page.localeCode}/${page.path}`)\n  } else {\n    return res.redirect(`/${page.path}`)\n  }\n})\n\n/**\n * Profile\n */\nrouter.get(['/p', '/p/*'], (req, res, next) => {\n  if (!req.user || req.user.id < 1 || req.user.id === 2) {\n    return res.status(403).render('unauthorized', { action: 'view' })\n  }\n\n  _.set(res.locals, 'pageMeta.title', 'User Profile')\n  res.render('profile')\n})\n\n/**\n * Source\n */\nrouter.get(['/s', '/s/*'], async (req, res, next) => {\n  const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })\n  const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0\n\n  const page = await WIKI.models.pages.getPageFromDb({\n    path: pageArgs.path,\n    locale: pageArgs.locale,\n    userId: req.user.id,\n    isPrivate: false\n  })\n\n  pageArgs.tags = _.get(page, 'tags', [])\n\n  if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {\n    return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)\n  }\n\n  // -> Effective Permissions\n  const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)\n\n  _.set(res, 'locals.siteConfig.lang', pageArgs.locale)\n  _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')\n\n  if (versionId > 0) {\n    if (!effectivePermissions.history.read) {\n      _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n      return res.status(403).render('unauthorized', { action: 'sourceVersion' })\n    }\n  } else {\n    if (!effectivePermissions.source.read) {\n      _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n      return res.status(403).render('unauthorized', { action: 'source' })\n    }\n  }\n\n  if (page) {\n    if (versionId > 0) {\n      const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId })\n      _.set(res.locals, 'pageMeta.title', pageVersion.title)\n      _.set(res.locals, 'pageMeta.description', pageVersion.description)\n      res.render('source', {\n        page: {\n          ...page,\n          ...pageVersion\n        },\n        effectivePermissions\n      })\n    } else {\n      _.set(res.locals, 'pageMeta.title', page.title)\n      _.set(res.locals, 'pageMeta.description', page.description)\n\n      res.render('source', { page, effectivePermissions })\n    }\n  } else {\n    res.redirect(`/${pageArgs.path}`)\n  }\n})\n\n/**\n * Tags\n */\nrouter.get(['/t', '/t/*'], (req, res, next) => {\n  _.set(res.locals, 'pageMeta.title', 'Tags')\n  res.render('tags')\n})\n\n/**\n * User Avatar\n */\nrouter.get('/_userav/:uid', async (req, res, next) => {\n  if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {\n    return res.sendStatus(403)\n  }\n  const av = await WIKI.models.users.getUserAvatarData(req.params.uid)\n  if (av) {\n    res.set('Content-Type', 'image/jpeg')\n    res.send(av)\n  }\n\n  return res.sendStatus(404)\n})\n\n/**\n * View document / asset\n */\nrouter.get('/*', async (req, res, next) => {\n  const stripExt = _.some(WIKI.config.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))\n  const pageArgs = pageHelper.parsePath(req.path, { stripExt })\n  const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)\n\n  if (isPage) {\n    if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {\n      const query = !_.isEmpty(req.query) ? `?${qs.stringify(req.query)}` : ''\n      return res.redirect(`/${pageArgs.locale}/${pageArgs.path}${query}`)\n    }\n\n    req.i18n.changeLanguage(pageArgs.locale)\n\n    try {\n      // -> Get Page from cache\n      const page = await WIKI.models.pages.getPage({\n        path: pageArgs.path,\n        locale: pageArgs.locale,\n        userId: req.user.id,\n        isPrivate: false\n      })\n      pageArgs.tags = _.get(page, 'tags', [])\n\n      // -> Effective Permissions\n      const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)\n\n      // -> Check User Access\n      if (!effectivePermissions.pages.read) {\n        if (req.user.id === 2) {\n          res.cookie('loginRedirect', req.path, {\n            maxAge: 15 * 60 * 1000\n          })\n        }\n        if (pageArgs.path === 'home' && req.user.id === 2) {\n          return res.redirect('/login')\n        }\n        _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n        return res.status(403).render('unauthorized', {\n          action: 'view'\n        })\n      }\n\n      _.set(res, 'locals.siteConfig.lang', pageArgs.locale)\n      _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')\n\n      if (page) {\n        _.set(res.locals, 'pageMeta.title', page.title)\n        _.set(res.locals, 'pageMeta.description', page.description)\n\n        // -> Check Publishing State\n        let pageIsPublished = page.isPublished\n        if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {\n          pageIsPublished = moment(page.publishStartDate).isSameOrBefore()\n        }\n        if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {\n          pageIsPublished = moment(page.publishEndDate).isSameOrAfter()\n        }\n        if (!pageIsPublished && !effectivePermissions.pages.write) {\n          _.set(res.locals, 'pageMeta.title', 'Unauthorized')\n          return res.status(403).render('unauthorized', {\n            action: 'view'\n          })\n        }\n\n        // -> Build sidebar navigation\n        let sdi = 1\n        const sidebar = (await WIKI.models.navigation.getTree({ cache: true, locale: pageArgs.locale, groups: req.user.groups })).map(n => ({\n          i: `sdi-${sdi++}`,\n          k: n.kind,\n          l: n.label,\n          c: n.icon,\n          y: n.targetType,\n          t: n.target\n        }))\n\n        // -> Build theme code injection\n        const injectCode = {\n          css: WIKI.config.theming.injectCSS,\n          head: WIKI.config.theming.injectHead,\n          body: WIKI.config.theming.injectBody\n        }\n\n        // Handle missing extra field\n        page.extra = page.extra || { css: '', js: '' }\n\n        if (!_.isEmpty(page.extra.css)) {\n          injectCode.css = `${injectCode.css}\\n${page.extra.css}`\n        }\n\n        if (!_.isEmpty(page.extra.js)) {\n          injectCode.body = `${injectCode.body}\\n${page.extra.js}`\n        }\n\n        if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) {\n          // -> Convert page TOC\n          if (_.isString(page.toc)) {\n            page.toc = JSON.parse(page.toc)\n          }\n\n          // -> Render legacy view\n          res.render('legacy/page', {\n            page,\n            sidebar,\n            injectCode,\n            isAuthenticated: req.user && req.user.id !== 2\n          })\n        } else {\n          // -> Convert page TOC\n          if (!_.isString(page.toc)) {\n            page.toc = JSON.stringify(page.toc)\n          }\n\n          // -> Inject comments variables\n          const commentTmpl = {\n            codeTemplate: WIKI.data.commentProvider.codeTemplate,\n            head: WIKI.data.commentProvider.head,\n            body: WIKI.data.commentProvider.body,\n            main: WIKI.data.commentProvider.main\n          }\n          if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {\n            [\n              { key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },\n              { key: 'pageId', value: page.id }\n            ].forEach((cfg) => {\n              commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)\n              commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)\n              commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)\n            })\n          }\n\n          // -> Page Filename (for edit on external repo button)\n          let pageFilename = WIKI.config.lang.namespacing ? `${pageArgs.locale}/${page.path}` : page.path\n          pageFilename += page.contentType === 'markdown' ? '.md' : '.html'\n\n          // -> Render view\n          res.render('page', {\n            page,\n            sidebar,\n            injectCode,\n            comments: commentTmpl,\n            effectivePermissions,\n            pageFilename\n          })\n        }\n      } else if (pageArgs.path === 'home') {\n        _.set(res.locals, 'pageMeta.title', 'Welcome')\n        res.render('welcome', { locale: pageArgs.locale })\n      } else {\n        _.set(res.locals, 'pageMeta.title', 'Page Not Found')\n        if (effectivePermissions.pages.write) {\n          res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })\n        } else {\n          res.status(404).render('notfound', { action: 'view' })\n        }\n      }\n    } catch (err) {\n      next(err)\n    }\n  } else {\n    if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {\n      return res.sendStatus(403)\n    }\n\n    await WIKI.models.assets.getAsset(pageArgs.path, res)\n  }\n})\n\nmodule.exports = router\n"
  },
  {
    "path": "server/controllers/ssl.js",
    "content": "const express = require('express')\nconst router = express.Router()\nconst _ = require('lodash')\nconst qs = require('querystring')\n\n/* global WIKI */\n\n/**\n * Let's Encrypt Challenge\n */\nrouter.get('/.well-known/acme-challenge/:token', (req, res, next) => {\n  res.type('text/plain')\n  if (_.get(WIKI.config, 'letsencrypt.challenge', false)) {\n    if (WIKI.config.letsencrypt.challenge.token === req.params.token) {\n      res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)\n      WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)\n    } else {\n      res.status(406).send('Invalid Challenge Token!')\n      WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)\n    }\n  } else {\n    res.status(418).end()\n  }\n})\n\n/**\n * Redirect to HTTPS if HTTP Redirection is enabled\n */\nrouter.all('/*', (req, res, next) => {\n  if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {\n    return res.redirect(`https://${req.hostname}${req.originalUrl}`)\n  } else {\n    next()\n  }\n})\n\nmodule.exports = router\n"
  },
  {
    "path": "server/controllers/upload.js",
    "content": "const express = require('express')\nconst router = express.Router()\nconst _ = require('lodash')\nconst multer = require('multer')\nconst path = require('path')\nconst sanitize = require('sanitize-filename')\n\n/* global WIKI */\n\n/**\n * Upload files\n */\nrouter.post('/u', (req, res, next) => {\n  multer({\n    dest: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'),\n    limits: {\n      fileSize: WIKI.config.uploads.maxFileSize,\n      files: WIKI.config.uploads.maxFiles\n    }\n  }).array('mediaUpload')(req, res, next)\n}, async (req, res, next) => {\n  if (!_.some(req.user.permissions, pm => _.includes(['write:assets', 'manage:system'], pm))) {\n    return res.status(403).json({\n      succeeded: false,\n      message: 'You are not authorized to upload files.'\n    })\n  } else if (req.files.length < 1) {\n    return res.status(400).json({\n      succeeded: false,\n      message: 'Missing upload payload.'\n    })\n  } else if (req.files.length > 1) {\n    return res.status(400).json({\n      succeeded: false,\n      message: 'You cannot upload multiple files within the same request.'\n    })\n  }\n  const fileMeta = _.get(req, 'files[0]', false)\n  if (!fileMeta) {\n    return res.status(500).json({\n      succeeded: false,\n      message: 'Missing upload file metadata.'\n    })\n  }\n\n  // Get folder Id\n  let folderId = null\n  try {\n    const folderRaw = _.get(req, 'body.mediaUpload', false)\n    if (folderRaw) {\n      folderId = _.get(JSON.parse(folderRaw), 'folderId', null)\n      if (folderId === 0) {\n        folderId = null\n      }\n    } else {\n      throw new Error('Missing File Metadata')\n    }\n  } catch (err) {\n    return res.status(400).json({\n      succeeded: false,\n      message: 'Missing upload folder metadata.'\n    })\n  }\n\n  // Build folder hierarchy\n  let hierarchy = []\n  if (folderId) {\n    try {\n      hierarchy = await WIKI.models.assetFolders.getHierarchy(folderId)\n    } catch (err) {\n      return res.status(400).json({\n        succeeded: false,\n        message: 'Failed to fetch folder hierarchy.'\n      })\n    }\n  }\n\n  // Sanitize filename\n  fileMeta.originalname = sanitize(fileMeta.originalname.toLowerCase().replace(/[\\s,;#]+/g, '_'))\n\n  // Check if user can upload at path\n  const assetPath = (folderId) ? hierarchy.map(h => h.slug).join('/') + `/${fileMeta.originalname}` : fileMeta.originalname\n  if (!WIKI.auth.checkAccess(req.user, ['write:assets'], { path: assetPath })) {\n    return res.status(403).json({\n      succeeded: false,\n      message: 'You are not authorized to upload files to this folder.'\n    })\n  }\n\n  // Process upload file\n  await WIKI.models.assets.upload({\n    ...fileMeta,\n    mode: 'upload',\n    folderId: folderId,\n    assetPath,\n    user: req.user\n  })\n  res.send('ok')\n})\n\nrouter.get('/u', async (req, res, next) => {\n  res.json({\n    ok: true\n  })\n})\n\nmodule.exports = router\n"
  },
  {
    "path": "server/core/asar.js",
    "content": "const pickle = require('chromium-pickle-js')\nconst path = require('path')\nconst UINT64 = require('cuint').UINT64\nconst fs = require('fs')\n\n/* global WIKI */\n\n/**\n * Based of express-serve-asar (https://github.com/toyobayashi/express-serve-asar)\n * by Fenglin Li (https://github.com/toyobayashi)\n */\n\nconst packages = {\n  'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`)\n}\n\nmodule.exports = {\n  fdCache: {},\n  async serve (pkgName, req, res, next) {\n    const file = this.readFilesystemSync(packages[pkgName])\n    const { filesystem, fd } = file\n    const info = filesystem.getFile(req.path.substring(1))\n    if (info) {\n      res.set({\n        'Content-Type': 'image/svg+xml',\n        'Content-Length': info.size\n      })\n\n      fs.createReadStream('', {\n        fd,\n        autoClose: false,\n        start: 8 + filesystem.headerSize + parseInt(info.offset, 10),\n        end: 8 + filesystem.headerSize + parseInt(info.offset, 10) + info.size - 1\n      }).on('error', (err) => {\n        WIKI.logger.warn(err)\n        res.sendStatus(404)\n      }).pipe(res.status(200))\n    } else {\n      res.sendStatus(404)\n    }\n  },\n  async unload () {\n    const fds = Object.values(this.fdCache)\n    if (fds.length > 0) {\n      WIKI.logger.info('Closing ASAR file descriptors...')\n      const closeAsync = require('util').promisify(fs.close)\n      await Promise.all(fds.map(x => closeAsync(x.fd)))\n      this.fdCache = {}\n    }\n  },\n  readArchiveHeaderSync (fd) {\n    let size\n    let headerBuf\n\n    const sizeBuf = Buffer.alloc(8)\n    if (fs.readSync(fd, sizeBuf, 0, 8, null) !== 8) {\n      throw new Error('Unable to read header size')\n    }\n\n    const sizePickle = pickle.createFromBuffer(sizeBuf)\n    size = sizePickle.createIterator().readUInt32()\n    headerBuf = Buffer.alloc(size)\n    if (fs.readSync(fd, headerBuf, 0, size, null) !== size) {\n      throw new Error('Unable to read header')\n    }\n\n    const headerPickle = pickle.createFromBuffer(headerBuf)\n    const header = headerPickle.createIterator().readString()\n    return { header: JSON.parse(header), headerSize: size }\n  },\n  readFilesystemSync (archive) {\n    if (!this.fdCache[archive]) {\n      const fd = fs.openSync(archive, 'r')\n      const header = this.readArchiveHeaderSync(fd)\n      const filesystem = new Filesystem(archive)\n      filesystem.header = header.header\n      filesystem.headerSize = header.headerSize\n      this.fdCache[archive] = {\n        fd,\n        filesystem: filesystem\n      }\n    }\n\n    return this.fdCache[archive]\n  }\n}\n\nclass Filesystem {\n  constructor (src) {\n    this.src = path.resolve(src)\n    this.header = { files: {} }\n    this.offset = UINT64(0)\n  }\n\n  searchNodeFromDirectory (p) {\n    let json = this.header\n    const dirs = p.split(path.sep)\n    for (const dir of dirs) {\n      if (dir !== '.') {\n        json = json.files[dir]\n      }\n    }\n    return json\n  }\n\n  getNode (p) {\n    const node = this.searchNodeFromDirectory(path.dirname(p))\n    const name = path.basename(p)\n    if (name) {\n      return node.files[name]\n    } else {\n      return node\n    }\n  }\n\n  getFile (p, followLinks) {\n    followLinks = typeof followLinks === 'undefined' ? true : followLinks\n    const info = this.getNode(p)\n\n    if (!info) {\n      return false\n    }\n\n    if (info.link && followLinks) {\n      return this.getFile(info.link)\n    } else {\n      return info\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/auth.js",
    "content": "const passport = require('passport')\nconst passportJWT = require('passport-jwt')\nconst _ = require('lodash')\nconst jwt = require('jsonwebtoken')\nconst ms = require('ms')\nconst { DateTime } = require('luxon')\nconst crypto = require('crypto')\nconst pem2jwk = require('pem-jwk').pem2jwk\nconst randomBytesAsync = require('util').promisify(crypto.randomBytes)\n\nconst commonHelper = require('../helpers/common')\nconst securityHelper = require('../helpers/security')\n\n/* global WIKI */\n\nmodule.exports = {\n  strategies: {},\n  guest: {\n    cacheExpiration: DateTime.utc().minus({ days: 1 })\n  },\n  groups: {},\n  validApiKeys: [],\n  revocationList: require('./cache').init(),\n\n  /**\n   * Initialize the authentication module\n   */\n  init() {\n    this.passport = passport\n\n    passport.serializeUser((user, done) => {\n      done(null, user.id)\n    })\n\n    passport.deserializeUser(async (id, done) => {\n      try {\n        const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => {\n          builder.select('groups.id', 'permissions')\n        })\n        if (user) {\n          done(null, user)\n        } else {\n          done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)\n        }\n      } catch (err) {\n        done(err, null)\n      }\n    })\n\n    this.reloadGroups()\n    this.reloadApiKeys()\n\n    return this\n  },\n\n  /**\n   * Load authentication strategies\n   */\n  async activateStrategies () {\n    try {\n      // Unload any active strategies\n      WIKI.auth.strategies = {}\n      const currentStrategies = _.keys(passport._strategies)\n      _.pull(currentStrategies, 'session')\n      _.forEach(currentStrategies, stg => { passport.unuse(stg) })\n\n      // Load JWT\n      passport.use('jwt', new passportJWT.Strategy({\n        jwtFromRequest: securityHelper.extractJWT,\n        secretOrKey: WIKI.config.certs.public,\n        audience: WIKI.config.auth.audience,\n        issuer: 'urn:wiki.js',\n        algorithms: ['RS256']\n      }, (jwtPayload, cb) => {\n        cb(null, jwtPayload)\n      }))\n\n      // Load enabled strategies\n      const enabledStrategies = await WIKI.models.authentication.getStrategies()\n      for (let idx in enabledStrategies) {\n        const stg = enabledStrategies[idx]\n        try {\n          const strategy = require(`../modules/authentication/${stg.strategyKey}/authentication.js`)\n\n          stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`\n          stg.config.key = stg.key\n          strategy.init(passport, stg.config)\n          strategy.config = stg.config\n\n          WIKI.auth.strategies[stg.key] = {\n            ...strategy,\n            ...stg\n          }\n          WIKI.logger.info(`Authentication Strategy ${stg.displayName}: [ OK ]`)\n        } catch (err) {\n          WIKI.logger.error(`Authentication Strategy ${stg.displayName} (${stg.key}): [ FAILED ]`)\n          WIKI.logger.error(err)\n        }\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`)\n      WIKI.logger.error(err)\n    }\n  },\n\n  /**\n   * Authenticate current request\n   *\n   * @param {Express Request} req\n   * @param {Express Response} res\n   * @param {Express Next Callback} next\n   */\n  authenticate (req, res, next) {\n    WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {\n      if (err) { return next() }\n      let mustRevalidate = false\n\n      // Expired but still valid within N days, just renew\n      if (info instanceof Error && info.name === 'TokenExpiredError') {\n        const expiredDate = (info.expiredAt instanceof Date) ? info.expiredAt.toISOString() : info.expiredAt\n        if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) < DateTime.fromISO(expiredDate)) {\n          mustRevalidate = true\n        }\n      }\n\n      // Check if user / group is in revocation list\n      if (user && !user.api && !mustRevalidate) {\n        const uRevalidate = WIKI.auth.revocationList.get(`u${_.toString(user.id)}`)\n        if (uRevalidate && user.iat < uRevalidate) {\n          mustRevalidate = true\n        } else if (DateTime.fromSeconds(user.iat) <= WIKI.startedAt) { // Prevent new / restarted instance from allowing revoked tokens\n          mustRevalidate = true\n        } else {\n          for (const gid of user.groups) {\n            const gRevalidate = WIKI.auth.revocationList.get(`g${_.toString(gid)}`)\n            if (gRevalidate && user.iat < gRevalidate) {\n              mustRevalidate = true\n              break\n            }\n          }\n        }\n      }\n\n      // Revalidate and renew token\n      if (mustRevalidate) {\n        const jwtPayload = jwt.decode(securityHelper.extractJWT(req))\n        try {\n          const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)\n          user = newToken.user\n          user.permissions = user.getGlobalPermissions()\n          user.groups = user.getGroups()\n          req.user = user\n\n          // Try headers, otherwise cookies for response\n          if (req.get('content-type') === 'application/json') {\n            res.set('new-jwt', newToken.token)\n          } else {\n            res.cookie('jwt', newToken.token, commonHelper.getCookieOpts())\n          }\n\n          // Avoid caching this response\n          res.set('Cache-Control', 'no-store')\n        } catch (errc) {\n          WIKI.logger.warn(errc)\n          return next()\n        }\n      }\n\n      // JWT is NOT valid, set as guest\n      if (!user) {\n        if (WIKI.auth.guest.cacheExpiration <= DateTime.utc()) {\n          WIKI.auth.guest = await WIKI.models.users.getGuestUser()\n          WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })\n        }\n        req.user = WIKI.auth.guest\n        return next()\n      }\n\n      // Process API tokens\n      if (_.has(user, 'api')) {\n        if (!WIKI.config.api.isEnabled) {\n          return next(new Error('API is disabled. You must enable it from the Administration Area first.'))\n        } else if (_.includes(WIKI.auth.validApiKeys, user.api)) {\n          req.user = {\n            id: 1,\n            email: 'api@localhost',\n            name: 'API',\n            pictureUrl: null,\n            timezone: 'America/New_York',\n            localeCode: 'en',\n            permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),\n            groups: [user.grp],\n            getGlobalPermissions () {\n              return req.user.permissions\n            },\n            getGroups () {\n              return req.user.groups\n            }\n          }\n          return next()\n        } else {\n          return next(new Error('API Key is invalid or was revoked.'))\n        }\n      }\n\n      // JWT is valid\n      req.logIn(user, { session: false }, (errc) => {\n        if (errc) { return next(errc) }\n        next()\n      })\n    })(req, res, next)\n  },\n\n  /**\n   * Check if user has access to resource\n   *\n   * @param {User} user\n   * @param {Array<String>} permissions\n   * @param {String|Boolean} path\n   */\n  checkAccess(user, permissions = [], page = false) {\n    const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()\n\n    // System Admin\n    if (_.includes(userPermissions, 'manage:system')) {\n      return true\n    }\n\n    // Check Global Permissions\n    if (_.intersection(userPermissions, permissions).length < 1) {\n      return false\n    }\n\n    // Skip if no page rule to check\n    if (!page) {\n      return true\n    }\n\n    // Check Page Rules\n    if (user.groups) {\n      let checkState = {\n        deny: false,\n        match: false,\n        specificity: ''\n      }\n      user.groups.forEach(grp => {\n        const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp\n        _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {\n          if (rule.locales && rule.locales.length > 0) {\n            if (!rule.locales.includes(page.locale)) { return }\n          }\n          if (_.intersection(rule.roles, permissions).length > 0) {\n            switch (rule.match) {\n              case 'START':\n                if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {\n                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })\n                }\n                break\n              case 'END':\n                if (_.endsWith(page.path, rule.path)) {\n                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })\n                }\n                break\n              case 'REGEX':\n                const reg = new RegExp(rule.path)\n                if (reg.test(page.path)) {\n                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] })\n                }\n                break\n              case 'TAG':\n                _.get(page, 'tags', []).forEach(tag => {\n                  if (tag.tag === rule.path) {\n                    checkState = this._applyPageRuleSpecificity({\n                      rule,\n                      checkState,\n                      higherPriority: ['EXACT']\n                    })\n                  }\n                })\n                break\n              case 'EXACT':\n                if (`/${page.path}` === `/${rule.path}`) {\n                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })\n                }\n                break\n            }\n          }\n        })\n      })\n\n      return (checkState.match && !checkState.deny)\n    }\n\n    return false\n  },\n\n  /**\n   * Check for exclusive permissions (contain any X permission(s) but not any Y permission(s))\n   *\n   * @param {User} user\n   * @param {Array<String>} includePermissions\n   * @param {Array<String>} excludePermissions\n   */\n  checkExclusiveAccess(user, includePermissions = [], excludePermissions = []) {\n    const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()\n\n    // Check Inclusion Permissions\n    if (_.intersection(userPermissions, includePermissions).length < 1) {\n      return false\n    }\n\n    // Check Exclusion Permissions\n    if (_.intersection(userPermissions, excludePermissions).length > 0) {\n      return false\n    }\n\n    return true\n  },\n\n  /**\n   * Check and apply Page Rule specificity\n   *\n   * @access private\n   */\n  _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {\n    if (rule.path.length === checkState.specificity.length) {\n      // Do not override higher priority rules\n      if (_.includes(higherPriority, checkState.match)) {\n        return checkState\n      }\n      // Do not override a previous DENY rule with same match\n      if (rule.match === checkState.match && checkState.deny && !rule.deny) {\n        return checkState\n      }\n    } else if (rule.path.length < checkState.specificity.length) {\n      // Do not override higher specificity rules\n      return checkState\n    }\n\n    return {\n      deny: rule.deny,\n      match: rule.match,\n      specificity: rule.path\n    }\n  },\n\n  /**\n   * Reload Groups from DB\n   */\n  async reloadGroups () {\n    const groupsArray = await WIKI.models.groups.query()\n    this.groups = _.keyBy(groupsArray, 'id')\n    WIKI.auth.guest.cacheExpiration = DateTime.utc().minus({ days: 1 })\n  },\n\n  /**\n   * Reload valid API Keys from DB\n   */\n  async reloadApiKeys () {\n    const keys = await WIKI.models.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', DateTime.utc().toISO())\n    this.validApiKeys = _.map(keys, 'id')\n  },\n\n  /**\n   * Generate New Authentication Public / Private Key Certificates\n   */\n  async regenerateCertificates () {\n    WIKI.logger.info('Regenerating certificates...')\n\n    _.set(WIKI.config, 'sessionSecret', (await randomBytesAsync(32)).toString('hex'))\n    const certs = crypto.generateKeyPairSync('rsa', {\n      modulusLength: 2048,\n      publicKeyEncoding: {\n        type: 'pkcs1',\n        format: 'pem'\n      },\n      privateKeyEncoding: {\n        type: 'pkcs1',\n        format: 'pem',\n        cipher: 'aes-256-cbc',\n        passphrase: WIKI.config.sessionSecret\n      }\n    })\n\n    _.set(WIKI.config, 'certs', {\n      jwk: pem2jwk(certs.publicKey),\n      public: certs.publicKey,\n      private: certs.privateKey\n    })\n\n    await WIKI.configSvc.saveToDb([\n      'certs',\n      'sessionSecret'\n    ])\n\n    await WIKI.auth.activateStrategies()\n    WIKI.events.outbound.emit('reloadAuthStrategies')\n\n    WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')\n  },\n\n  /**\n   * Reset Guest User\n   */\n  async resetGuestUser() {\n    WIKI.logger.info('Resetting guest account...')\n    const guestGroup = await WIKI.models.groups.query().where('id', 2).first()\n\n    await WIKI.models.users.query().delete().where({\n      providerKey: 'local',\n      email: 'guest@example.com'\n    }).orWhere('id', 2)\n\n    const guestUser = await WIKI.models.users.query().insert({\n      id: 2,\n      provider: 'local',\n      email: 'guest@example.com',\n      name: 'Guest',\n      password: '',\n      locale: 'en',\n      defaultEditor: 'markdown',\n      tfaIsActive: false,\n      isSystem: true,\n      isActive: true,\n      isVerified: true\n    })\n    await guestUser.$relatedQuery('groups').relate(guestGroup.id)\n\n    WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')\n  },\n\n  /**\n   * Subscribe to HA propagation events\n   */\n  subscribeToEvents() {\n    WIKI.events.inbound.on('reloadGroups', () => {\n      WIKI.auth.reloadGroups()\n    })\n    WIKI.events.inbound.on('reloadApiKeys', () => {\n      WIKI.auth.reloadApiKeys()\n    })\n    WIKI.events.inbound.on('reloadAuthStrategies', () => {\n      WIKI.auth.activateStrategies()\n    })\n    WIKI.events.inbound.on('addAuthRevoke', (args) => {\n      WIKI.auth.revokeUserTokens(args)\n    })\n  },\n\n  /**\n   * Get all user permissions for a specific page\n   */\n  getEffectivePermissions (req, page) {\n    return {\n      comments: {\n        read: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['read:comments'], page) : false,\n        write: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['write:comments'], page) : false,\n        manage: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['manage:comments'], page) : false\n      },\n      history: {\n        read: WIKI.auth.checkAccess(req.user, ['read:history'], page)\n      },\n      source: {\n        read: WIKI.auth.checkAccess(req.user, ['read:source'], page)\n      },\n      pages: {\n        read: WIKI.auth.checkAccess(req.user, ['read:pages'], page),\n        write: WIKI.auth.checkAccess(req.user, ['write:pages'], page),\n        manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page),\n        delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page),\n        script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page),\n        style: WIKI.auth.checkAccess(req.user, ['write:styles'], page)\n      },\n      system: {\n        manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page)\n      }\n    }\n  },\n\n  /**\n   * Add user / group ID to JWT revocation list, forcing all requests to be validated against the latest permissions\n   */\n  revokeUserTokens ({ id, kind = 'u' }) {\n    WIKI.auth.revocationList.set(`${kind}${_.toString(id)}`, Math.round(DateTime.utc().minus({ seconds: 5 }).toSeconds()), Math.ceil(ms(WIKI.config.auth.tokenExpiration) / 1000))\n  }\n}\n"
  },
  {
    "path": "server/core/cache.js",
    "content": "const NodeCache = require('node-cache')\n\nmodule.exports = {\n  init() {\n    return new NodeCache()\n  }\n}\n"
  },
  {
    "path": "server/core/config.js",
    "content": "const _ = require('lodash')\nconst chalk = require('chalk')\nconst cfgHelper = require('../helpers/config')\nconst fs = require('fs')\nconst path = require('path')\nconst yaml = require('js-yaml')\n\n/* global WIKI */\n\nmodule.exports = {\n  /**\n   * Load root config from disk\n   */\n  init() {\n    let confPaths = {\n      config: path.join(WIKI.ROOTPATH, 'config.yml'),\n      data: path.join(WIKI.SERVERPATH, 'app/data.yml'),\n      dataRegex: path.join(WIKI.SERVERPATH, 'app/regex.js')\n    }\n\n    if (process.env.dockerdev) {\n      confPaths.config = path.join(WIKI.ROOTPATH, `dev/containers/config.yml`)\n    }\n\n    if (process.env.CONFIG_FILE) {\n      confPaths.config = path.resolve(WIKI.ROOTPATH, process.env.CONFIG_FILE)\n    }\n\n    process.stdout.write(chalk.blue(`Loading configuration from ${confPaths.config}... `))\n\n    let appconfig = {}\n    let appdata = {}\n\n    try {\n      appconfig = yaml.safeLoad(\n        cfgHelper.parseConfigValue(\n          fs.readFileSync(confPaths.config, 'utf8')\n        )\n      )\n      appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8'))\n      appdata.regex = require(confPaths.dataRegex)\n      console.info(chalk.green.bold(`OK`))\n    } catch (err) {\n      console.error(chalk.red.bold(`FAILED`))\n      console.error(err.message)\n\n      console.error(chalk.red.bold(`>>> Unable to read configuration file! Did you create the config.yml file?`))\n      process.exit(1)\n    }\n\n    // Merge with defaults\n\n    appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)\n\n    if (appconfig.port < 1 || process.env.HEROKU) {\n      appconfig.port = process.env.PORT || 80\n    }\n\n    const packageInfo = require(path.join(WIKI.ROOTPATH, 'package.json'))\n\n    // Load DB Password from Docker Secret File\n    if (process.env.DB_PASS_FILE) {\n      console.info(chalk.blue(`DB_PASS_FILE is defined. Will use secret from file.`))\n      try {\n        appconfig.db.pass = fs.readFileSync(process.env.DB_PASS_FILE, 'utf8').trim()\n      } catch (err) {\n        console.error(chalk.red.bold(`>>> Failed to read Docker Secret File using path defined in DB_PASS_FILE env variable!`))\n        console.error(err.message)\n        process.exit(1)\n      }\n    }\n\n    WIKI.config = appconfig\n    WIKI.data = appdata\n    WIKI.version = packageInfo.version\n    WIKI.releaseDate = packageInfo.releaseDate\n    WIKI.devMode = (packageInfo.dev === true)\n  },\n\n  /**\n   * Load config from DB\n   */\n  async loadFromDb() {\n    let conf = await WIKI.models.settings.getConfig()\n    if (conf) {\n      WIKI.config = _.defaultsDeep(conf, WIKI.config)\n    } else {\n      WIKI.logger.warn('DB Configuration is empty or incomplete. Switching to Setup mode...')\n      WIKI.config.setup = true\n    }\n  },\n  /**\n   * Save config to DB\n   *\n   * @param {Array} keys Array of keys to save\n   * @returns Promise\n   */\n  async saveToDb(keys, propagate = true) {\n    try {\n      for (let key of keys) {\n        let value = _.get(WIKI.config, key, null)\n        if (!_.isPlainObject(value)) {\n          value = { v: value }\n        }\n        let affectedRows = await WIKI.models.settings.query().patch({ value }).where('key', key)\n        if (affectedRows === 0 && value) {\n          await WIKI.models.settings.query().insert({ key, value })\n        }\n      }\n      if (propagate) {\n        WIKI.events.outbound.emit('reloadConfig')\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to save configuration to DB: ${err.message}`)\n      return false\n    }\n\n    return true\n  },\n  /**\n   * Apply Dev Flags\n   */\n  async applyFlags() {\n    WIKI.models.knex.client.config.debug = WIKI.config.flags.sqllog\n  },\n\n  /**\n   * Subscribe to HA propagation events\n   */\n  subscribeToEvents() {\n    WIKI.events.inbound.on('reloadConfig', async () => {\n      await WIKI.configSvc.loadFromDb()\n      await WIKI.configSvc.applyFlags()\n    })\n  }\n}\n"
  },
  {
    "path": "server/core/db.js",
    "content": "const _ = require('lodash')\nconst autoload = require('auto-load')\nconst path = require('path')\nconst Promise = require('bluebird')\nconst Knex = require('knex')\nconst fs = require('fs')\nconst Objection = require('objection')\n\nconst migrationSource = require('../db/migrator-source')\nconst migrateFromBeta = require('../db/beta')\n\n/* global WIKI */\n\n/**\n * ORM DB module\n */\nmodule.exports = {\n  Objection,\n  knex: null,\n  listener: null,\n  /**\n   * Initialize DB\n   *\n   * @return     {Object}  DB instance\n   */\n  init() {\n    let self = this\n\n    // Fetch DB Config\n\n    let dbClient = null\n    let dbConfig = (!_.isEmpty(process.env.DATABASE_URL)) ? process.env.DATABASE_URL : {\n      host: WIKI.config.db.host.toString(),\n      user: WIKI.config.db.user.toString(),\n      password: WIKI.config.db.pass.toString(),\n      database: WIKI.config.db.db.toString(),\n      port: WIKI.config.db.port\n    }\n\n    // Handle SSL Options\n\n    let dbUseSSL = (WIKI.config.db.ssl === true || WIKI.config.db.ssl === 'true' || WIKI.config.db.ssl === 1 || WIKI.config.db.ssl === '1')\n    let sslOptions = null\n    if (dbUseSSL && _.isPlainObject(dbConfig) && _.get(WIKI.config.db, 'sslOptions.auto', null) === false) {\n      sslOptions = WIKI.config.db.sslOptions\n      sslOptions.rejectUnauthorized = sslOptions.rejectUnauthorized !== false\n      if (sslOptions.ca && sslOptions.ca.indexOf('-----') !== 0) {\n        sslOptions.ca = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.ca))\n      }\n      if (sslOptions.cert) {\n        sslOptions.cert = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.cert))\n      }\n      if (sslOptions.key) {\n        sslOptions.key = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.key))\n      }\n      if (sslOptions.pfx) {\n        sslOptions.pfx = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.pfx))\n      }\n    } else {\n      sslOptions = true\n    }\n\n    // Handle inline SSL CA Certificate mode\n    if (!_.isEmpty(process.env.DB_SSL_CA)) {\n      const chunks = []\n      for (let i = 0, charsLength = process.env.DB_SSL_CA.length; i < charsLength; i += 64) {\n        chunks.push(process.env.DB_SSL_CA.substring(i, i + 64))\n      }\n\n      dbUseSSL = true\n      sslOptions = {\n        rejectUnauthorized: true,\n        ca: '-----BEGIN CERTIFICATE-----\\n' + chunks.join('\\n') + '\\n-----END CERTIFICATE-----\\n'\n      }\n    }\n\n    // Engine-specific config\n    switch (WIKI.config.db.type) {\n      case 'postgres':\n        dbClient = 'pg'\n\n        if (dbUseSSL && _.isPlainObject(dbConfig)) {\n          dbConfig.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions\n        }\n        break\n      case 'mariadb':\n      case 'mysql':\n        dbClient = 'mysql2'\n\n        if (dbUseSSL && _.isPlainObject(dbConfig)) {\n          dbConfig.ssl = sslOptions\n        }\n\n        // Prune host and port if socketPath is configured\n        if (WIKI.config.db.socketPath) {\n          const { host, port, ...prunedConfig } = dbConfig\n          dbConfig = prunedConfig\n          dbConfig.socketPath = WIKI.config.db.socketPath.toString()\n        }\n\n        // Fix mysql boolean handling...\n        dbConfig.typeCast = (field, next) => {\n          if (field.type === 'TINY' && field.length === 1) {\n            let value = field.string()\n            return value ? (value === '1') : null\n          }\n          return next()\n        }\n        break\n      case 'mssql':\n        dbClient = 'mssql'\n\n        if (_.isPlainObject(dbConfig)) {\n          dbConfig.appName = 'Wiki.js'\n          _.set(dbConfig, 'options.appName', 'Wiki.js')\n\n          dbConfig.enableArithAbort = true\n          _.set(dbConfig, 'options.enableArithAbort', true)\n\n          if (dbUseSSL) {\n            dbConfig.encrypt = true\n            _.set(dbConfig, 'options.encrypt', true)\n          }\n        }\n        break\n      case 'sqlite':\n        dbClient = 'sqlite3'\n        dbConfig = { filename: WIKI.config.db.storage }\n        break\n      default:\n        WIKI.logger.error('Invalid DB Type')\n        process.exit(1)\n    }\n\n    // Initialize Knex\n    this.knex = Knex({\n      client: dbClient,\n      useNullAsDefault: true,\n      asyncStackTraces: WIKI.IS_DEBUG,\n      connection: dbConfig,\n      pool: {\n        ...WIKI.config.pool,\n        async afterCreate(conn, done) {\n          // -> Set Connection App Name\n          switch (WIKI.config.db.type) {\n            case 'postgres':\n              await conn.query(`set application_name = 'Wiki.js'`)\n              // -> Set schema if it's not public\n              if (WIKI.config.db.schema && WIKI.config.db.schema !== 'public') {\n                await conn.query(`set search_path TO ${WIKI.config.db.schema}, public;`)\n              }\n              done()\n              break\n            case 'mysql':\n              await conn.promise().query(`set autocommit = 1`)\n              done()\n              break\n            default:\n              done()\n              break\n          }\n        }\n      },\n      debug: WIKI.IS_DEBUG\n    })\n\n    Objection.Model.knex(this.knex)\n\n    // Load DB Models\n\n    const models = autoload(path.join(WIKI.SERVERPATH, 'models'))\n\n    // Set init tasks\n    let conAttempts = 0\n    let initTasks = {\n      // -> Attempt initial connection\n      async connect () {\n        try {\n          WIKI.logger.info('Connecting to database...')\n          await self.knex.raw('SELECT 1 + 1;')\n          WIKI.logger.info('Database Connection Successful [ OK ]')\n        } catch (err) {\n          if (conAttempts < 10) {\n            if (err.code) {\n              WIKI.logger.error(`Database Connection Error: ${err.code} ${err.address}:${err.port}`)\n            } else {\n              WIKI.logger.error(`Database Connection Error: ${err.message}`)\n            }\n            WIKI.logger.warn(`Will retry in 3 seconds... [Attempt ${++conAttempts} of 10]`)\n            await new Promise(resolve => setTimeout(resolve, 3000))\n            await initTasks.connect()\n          } else {\n            throw err\n          }\n        }\n      },\n      // -> Migrate DB Schemas\n      async syncSchemas () {\n        return self.knex.migrate.latest({\n          tableName: 'migrations',\n          migrationSource\n        })\n      },\n      // -> Migrate DB Schemas from beta\n      async migrateFromBeta () {\n        return migrateFromBeta.migrate(self.knex)\n      }\n    }\n\n    let initTasksQueue = (WIKI.IS_MASTER) ? [\n      initTasks.connect,\n      initTasks.migrateFromBeta,\n      initTasks.syncSchemas\n    ] : [\n      () => { return Promise.resolve() }\n    ]\n\n    // Perform init tasks\n\n    WIKI.logger.info(`Using database driver ${dbClient} for ${WIKI.config.db.type} [ OK ]`)\n    this.onReady = Promise.each(initTasksQueue, t => t()).return(true)\n\n    return {\n      ...this,\n      ...models\n    }\n  },\n  /**\n   * Subscribe to database LISTEN / NOTIFY for multi-instances events\n   */\n  async subscribeToNotifications () {\n    const useHA = (WIKI.config.ha === true || (typeof WIKI.config.ha === 'string' && WIKI.config.ha.toLowerCase() === 'true') || WIKI.config.ha === 1 || WIKI.config.ha === '1')\n    if (!useHA) {\n      return\n    } else if (WIKI.config.db.type !== 'postgres') {\n      WIKI.logger.warn(`Database engine doesn't support pub/sub. Will not handle concurrent instances: [ DISABLED ]`)\n      return\n    }\n\n    const PGPubSub = require('pg-pubsub')\n\n    this.listener = new PGPubSub(this.knex.client.connectionSettings, {\n      log (ev) {\n        WIKI.logger.debug(ev)\n      }\n    })\n\n    // -> Outbound events handling\n\n    this.listener.addChannel('wiki', payload => {\n      if (_.has(payload, 'event') && payload.source !== WIKI.INSTANCE_ID) {\n        WIKI.logger.info(`Received event ${payload.event} from instance ${payload.source}: [ OK ]`)\n        WIKI.events.inbound.emit(payload.event, payload.value)\n      }\n    })\n    WIKI.events.outbound.onAny(this.notifyViaDB)\n\n    // -> Listen to inbound events\n\n    WIKI.auth.subscribeToEvents()\n    WIKI.configSvc.subscribeToEvents()\n    WIKI.models.pages.subscribeToEvents()\n\n    WIKI.logger.info(`High-Availability Listener initialized successfully: [ OK ]`)\n  },\n  /**\n   * Unsubscribe from database LISTEN / NOTIFY\n   */\n  async unsubscribeToNotifications () {\n    if (this.listener) {\n      WIKI.events.outbound.offAny(this.notifyViaDB)\n      WIKI.events.inbound.removeAllListeners()\n      this.listener.close()\n    }\n  },\n  /**\n   * Publish event via database NOTIFY\n   *\n   * @param {string} event Event fired\n   * @param {object} value Payload of the event\n   */\n  notifyViaDB (event, value) {\n    WIKI.models.listener.publish('wiki', {\n      source: WIKI.INSTANCE_ID,\n      event,\n      value\n    })\n  }\n}\n"
  },
  {
    "path": "server/core/extensions.js",
    "content": "const fs = require('fs-extra')\nconst path = require('path')\n\n/* global WIKI */\n\nmodule.exports = {\n  ext: {},\n  async init () {\n    const extDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/extensions'))\n    WIKI.logger.info(`Checking for installed optional extensions...`)\n    for (let dir of extDirs) {\n      WIKI.extensions.ext[dir] = require(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.js'))\n      const isInstalled = await WIKI.extensions.ext[dir].check()\n      if (isInstalled) {\n        WIKI.logger.info(`Optional extension ${dir} is installed. [ OK ]`)\n      } else {\n        WIKI.logger.info(`Optional extension ${dir} was not found on this system. [ SKIPPED ]`)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/kernel.js",
    "content": "const _ = require('lodash')\nconst EventEmitter = require('eventemitter2').EventEmitter2\n\n/* global WIKI */\n\nmodule.exports = {\n  async init() {\n    WIKI.logger.info('=======================================')\n    WIKI.logger.info(`= Wiki.js ${_.padEnd(WIKI.version + ' ', 29, '=')}`)\n    WIKI.logger.info('=======================================')\n    WIKI.logger.info('Initializing...')\n\n    WIKI.models = require('./db').init()\n\n    try {\n      await WIKI.models.onReady\n      await WIKI.configSvc.loadFromDb()\n      await WIKI.configSvc.applyFlags()\n    } catch (err) {\n      WIKI.logger.error('Database Initialization Error: ' + err.message)\n      if (WIKI.IS_DEBUG) {\n        WIKI.logger.error(err)\n      }\n      process.exit(1)\n    }\n\n    this.bootMaster()\n  },\n  /**\n   * Pre-Master Boot Sequence\n   */\n  async preBootMaster() {\n    try {\n      await this.initTelemetry()\n      WIKI.sideloader = await require('./sideloader').init()\n      WIKI.cache = require('./cache').init()\n      WIKI.scheduler = require('./scheduler').init()\n      WIKI.servers = require('./servers')\n      WIKI.events = {\n        inbound: new EventEmitter(),\n        outbound: new EventEmitter()\n      }\n      WIKI.extensions = require('./extensions')\n      WIKI.asar = require('./asar')\n    } catch (err) {\n      WIKI.logger.error(err)\n      process.exit(1)\n    }\n  },\n  /**\n   * Boot Master Process\n   */\n  async bootMaster() {\n    try {\n      if (WIKI.config.setup) {\n        WIKI.logger.info('Starting setup wizard...')\n        require('../setup')()\n      } else {\n        await this.preBootMaster()\n        await require('../master')()\n        this.postBootMaster()\n      }\n    } catch (err) {\n      WIKI.logger.error(err)\n      process.exit(1)\n    }\n  },\n  /**\n   * Post-Master Boot Sequence\n   */\n  async postBootMaster() {\n    await WIKI.models.analytics.refreshProvidersFromDisk()\n    await WIKI.models.authentication.refreshStrategiesFromDisk()\n    await WIKI.models.commentProviders.refreshProvidersFromDisk()\n    await WIKI.models.editors.refreshEditorsFromDisk()\n    await WIKI.models.loggers.refreshLoggersFromDisk()\n    await WIKI.models.renderers.refreshRenderersFromDisk()\n    await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()\n    await WIKI.models.storage.refreshTargetsFromDisk()\n\n    await WIKI.extensions.init()\n\n    await WIKI.auth.activateStrategies()\n    await WIKI.models.commentProviders.initProvider()\n    await WIKI.models.searchEngines.initEngine()\n    await WIKI.models.storage.initTargets()\n    WIKI.scheduler.start()\n\n    await WIKI.models.subscribeToNotifications()\n  },\n  /**\n   * Init Telemetry\n   */\n  async initTelemetry() {\n    require('./telemetry').init()\n\n    process.on('unhandledRejection', (err) => {\n      WIKI.logger.warn(err)\n      WIKI.telemetry.sendError(err)\n    })\n    process.on('uncaughtException', (err) => {\n      WIKI.logger.warn(err)\n      WIKI.telemetry.sendError(err)\n    })\n  },\n  /**\n   * Graceful shutdown\n   */\n  async shutdown (devMode = false) {\n    if (WIKI.servers) {\n      await WIKI.servers.stopServers()\n    }\n    if (WIKI.scheduler) {\n      await WIKI.scheduler.stop()\n    }\n    if (WIKI.models) {\n      await WIKI.models.unsubscribeToNotifications()\n      if (WIKI.models.knex) {\n        await WIKI.models.knex.destroy()\n      }\n    }\n    if (WIKI.asar) {\n      await WIKI.asar.unload()\n    }\n    if (!devMode) {\n      process.exit(0)\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/letsencrypt.js",
    "content": "const ACME = require('acme')\nconst Keypairs = require('@root/keypairs')\nconst _ = require('lodash')\nconst moment = require('moment')\nconst CSR = require('@root/csr')\nconst PEM = require('@root/pem')\n// eslint-disable-next-line node/no-deprecated-api\nconst punycode = require('punycode')\n\n/* global WIKI */\n\nmodule.exports = {\n  apiDirectory: WIKI.dev ? 'https://acme-staging-v02.api.letsencrypt.org/directory' : 'https://acme-v02.api.letsencrypt.org/directory',\n  acme: null,\n  async init () {\n    if (!_.get(WIKI.config, 'letsencrypt.payload', false)) {\n      await this.requestCertificate()\n    } else if (WIKI.config.letsencrypt.domain !== WIKI.config.ssl.domain) {\n      WIKI.logger.info(`(LETSENCRYPT) Domain has changed. Requesting new certificates...`)\n      await this.requestCertificate()\n    } else if (moment(WIKI.config.letsencrypt.payload.expires).isSameOrBefore(moment().add(5, 'days'))) {\n      WIKI.logger.info(`(LETSENCRYPT) Certificate is about to or has expired, requesting a new one...`)\n      await this.requestCertificate()\n    } else {\n      WIKI.logger.info(`(LETSENCRYPT) Using existing certificate for ${WIKI.config.ssl.domain}, expires on ${WIKI.config.letsencrypt.payload.expires}: [ OK ]`)\n    }\n    WIKI.config.ssl.format = 'pem'\n    WIKI.config.ssl.inline = true\n    WIKI.config.ssl.key = WIKI.config.letsencrypt.serverKey\n    WIKI.config.ssl.cert = WIKI.config.letsencrypt.payload.cert + '\\n' + WIKI.config.letsencrypt.payload.chain\n    WIKI.config.ssl.passphrase = null\n    WIKI.config.ssl.dhparam = null\n  },\n  async requestCertificate () {\n    try {\n      WIKI.logger.info(`(LETSENCRYPT) Initializing Let's Encrypt client...`)\n      this.acme = ACME.create({\n        maintainerEmail: WIKI.config.maintainerEmail,\n        packageAgent: `wikijs/${WIKI.version}`,\n        notify: (ev, msg) => {\n          if (_.includes(['warning', 'error'], ev)) {\n            WIKI.logger.warn(`${ev}: ${msg}`)\n          } else {\n            WIKI.logger.debug(`${ev}: ${JSON.stringify(msg)}`)\n          }\n        }\n      })\n\n      await this.acme.init(this.apiDirectory)\n\n      // -> Create ACME Subscriber account\n\n      if (!_.get(WIKI.config, 'letsencrypt.account', false)) {\n        WIKI.logger.info(`(LETSENCRYPT) Setting up account for the first time...`)\n        const accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' })\n        const account = await this.acme.accounts.create({\n          subscriberEmail: WIKI.config.ssl.subscriberEmail,\n          agreeToTerms: true,\n          accountKey: accountKeypair.private\n        })\n        WIKI.config.letsencrypt = {\n          accountKeypair: accountKeypair,\n          account: account,\n          domain: WIKI.config.ssl.domain\n        }\n        await WIKI.configSvc.saveToDb(['letsencrypt'])\n        WIKI.logger.info(`(LETSENCRYPT) Account was setup successfully [ OK ]`)\n      }\n\n      // -> Create Server Keypair\n\n      if (!WIKI.config.letsencrypt.serverKey) {\n        WIKI.logger.info(`(LETSENCRYPT) Generating server keypairs...`)\n        const serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' })\n        WIKI.config.letsencrypt.serverKey = await Keypairs.export({ jwk: serverKeypair.private })\n        WIKI.logger.info(`(LETSENCRYPT) Server keypairs generated successfully [ OK ]`)\n      }\n\n      // -> Create CSR\n\n      WIKI.logger.info(`(LETSENCRYPT) Generating certificate signing request (CSR)...`)\n      const domains = [ punycode.toASCII(WIKI.config.ssl.domain) ]\n      const serverKey = await Keypairs.import({ pem: WIKI.config.letsencrypt.serverKey })\n      const csrDer = await CSR.csr({ jwk: serverKey, domains, encoding: 'der' })\n      const csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: csrDer })\n      WIKI.logger.info(`(LETSENCRYPT) CSR generated successfully [ OK ]`)\n\n      // -> Verify Domain + Get Certificate\n\n      WIKI.logger.info(`(LETSENCRYPT) Requesting certificate from Let's Encrypt...`)\n      const certResp = await this.acme.certificates.create({\n        account: WIKI.config.letsencrypt.account,\n        accountKey: WIKI.config.letsencrypt.accountKeypair.private,\n        csr,\n        domains,\n        challenges: {\n          'http-01': {\n            init () {},\n            set (data) {\n              WIKI.logger.info(`(LETSENCRYPT) Setting HTTP challenge for ${data.challenge.hostname}: [ READY ]`)\n              WIKI.config.letsencrypt.challenge = data.challenge\n              WIKI.logger.info(`(LETSENCRYPT) Waiting for challenge to complete...`)\n              return null // <- this is needed, cannot be undefined\n            },\n            get (data) {\n              return WIKI.config.letsencrypt.challenge\n            },\n            async remove (data) {\n              WIKI.logger.info(`(LETSENCRYPT) Removing HTTP challenge: [ OK ]`)\n              WIKI.config.letsencrypt.challenge = null\n              return null // <- this is needed, cannot be undefined\n            }\n          }\n        }\n      })\n      WIKI.logger.info(`(LETSENCRYPT) New certificate received successfully: [ COMPLETED ]`)\n      WIKI.config.letsencrypt.payload = certResp\n      WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain\n      await WIKI.configSvc.saveToDb(['letsencrypt'])\n    } catch (err) {\n      WIKI.logger.warn(`(LETSENCRYPT) ${err}`)\n      throw err\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/localization.js",
    "content": "const _ = require('lodash')\nconst dotize = require('dotize')\nconst i18nMW = require('i18next-express-middleware')\nconst i18next = require('i18next')\nconst Promise = require('bluebird')\nconst fs = require('fs-extra')\nconst path = require('path')\nconst yaml = require('js-yaml')\n\n/* global WIKI */\n\nmodule.exports = {\n  engine: null,\n  namespaces: [],\n  init() {\n    this.namespaces = WIKI.data.localeNamespaces\n    this.engine = i18next\n    this.engine.init({\n      load: 'languageOnly',\n      ns: this.namespaces,\n      defaultNS: 'common',\n      saveMissing: false,\n      lng: WIKI.config.lang.code,\n      fallbackLng: 'en'\n    })\n\n    // Load current language + namespaces\n    this.refreshNamespaces(true)\n\n    return this\n  },\n  /**\n   * Attach i18n middleware for Express\n   *\n   * @param {Object} app Express Instance\n   */\n  attachMiddleware (app) {\n    app.use(i18nMW.handle(this.engine))\n  },\n  /**\n   * Get all entries for a specific locale and namespace\n   *\n   * @param {String} locale Locale code\n   * @param {String} namespace Namespace\n   */\n  async getByNamespace(locale, namespace) {\n    if (this.engine.hasResourceBundle(locale, namespace)) {\n      let data = this.engine.getResourceBundle(locale, namespace)\n      return _.map(dotize.convert(data), (value, key) => {\n        return {\n          key,\n          value\n        }\n      })\n    } else {\n      throw new Error('Invalid locale or namespace')\n    }\n  },\n  /**\n   * Load entries from the DB for a single locale\n   *\n   * @param {String} locale Locale code\n   * @param {*} opts Additional options\n   */\n  async loadLocale(locale, opts = { silent: false }) {\n    const res = await WIKI.models.locales.query().findOne('code', locale)\n    if (res) {\n      if (_.isPlainObject(res.strings)) {\n        _.forOwn(res.strings, (data, ns) => {\n          this.namespaces.push(ns)\n          this.engine.addResourceBundle(locale, ns, data, true, true)\n        })\n      }\n    } else if (!opts.silent) {\n      throw new Error('No such locale in local store.')\n    }\n\n    // -> Load dev locale files if present\n    if (WIKI.IS_DEBUG) {\n      try {\n        const devEntriesRaw = await fs.readFile(path.join(WIKI.SERVERPATH, `locales/${locale}.yml`), 'utf8')\n        if (devEntriesRaw) {\n          const devEntries = yaml.safeLoad(devEntriesRaw)\n          _.forOwn(devEntries, (data, ns) => {\n            this.namespaces.push(ns)\n            this.engine.addResourceBundle(locale, ns, data, true, true)\n          })\n          WIKI.logger.info(`Loaded dev locales from ${locale}.yml`)\n        }\n      } catch (err) {\n        // ignore\n      }\n    }\n  },\n  /**\n   * Reload all namespaces for all active locales from the DB\n   *\n   * @param {Boolean} silent No error on fail\n   */\n  async refreshNamespaces (silent = false) {\n    await this.loadLocale(WIKI.config.lang.code, { silent })\n    if (WIKI.config.lang.namespacing) {\n      for (let ns of WIKI.config.lang.namespaces) {\n        await this.loadLocale(ns, { silent })\n      }\n    }\n  },\n  /**\n   * Set the active locale\n   *\n   * @param {String} locale Locale code\n   */\n  async setCurrentLocale(locale) {\n    await Promise.fromCallback(cb => {\n      return this.engine.changeLanguage(locale, cb)\n    })\n  }\n}\n"
  },
  {
    "path": "server/core/logger.js",
    "content": "// const _ = require('lodash')\nconst winston = require('winston')\n\n/* global WIKI */\n\nmodule.exports = {\n  loggers: {},\n  init(uid) {\n    const loggerFormats = [\n      winston.format.label({ label: uid }),\n      winston.format.timestamp()\n    ]\n\n    if (WIKI.config.logFormat === 'json') {\n      loggerFormats.push(winston.format.json())\n    } else {\n      loggerFormats.push(winston.format.colorize())\n      loggerFormats.push(winston.format.printf(info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`))\n    }\n\n    const logger = winston.createLogger({\n      level: WIKI.config.logLevel,\n      format: winston.format.combine(...loggerFormats)\n    })\n\n    // Init Console (default)\n\n    logger.add(new winston.transports.Console({\n      level: WIKI.config.logLevel,\n      prettyPrint: true,\n      colorize: true,\n      silent: false,\n      timestamp: true\n    }))\n\n    // _.forOwn(_.omitBy(WIKI.config.logging.loggers, s => s.enabled === false), (loggerConfig, loggerKey) => {\n    //   let loggerModule = require(`../modules/logging/${loggerKey}`)\n    //   loggerModule.init(logger, loggerConfig)\n    //   this.loggers[logger.key] = loggerModule\n    // })\n\n    return logger\n  }\n}\n"
  },
  {
    "path": "server/core/mail.js",
    "content": "const nodemailer = require('nodemailer')\nconst _ = require('lodash')\nconst fs = require('fs-extra')\nconst path = require('path')\n\n/* global WIKI */\n\nmodule.exports = {\n  transport: null,\n  templates: {},\n  init() {\n    if (_.get(WIKI.config, 'mail.host', '').length > 2) {\n      let conf = {\n        host: WIKI.config.mail.host,\n        port: WIKI.config.mail.port,\n        name: WIKI.config.mail.name,\n        secure: WIKI.config.mail.secure,\n        tls: {\n          rejectUnauthorized: !(WIKI.config.mail.verifySSL === false)\n        }\n      }\n      if (_.get(WIKI.config, 'mail.user', '').length > 1) {\n        conf = {\n          ...conf,\n          auth: {\n            user: WIKI.config.mail.user,\n            pass: WIKI.config.mail.pass\n          }\n        }\n      }\n      if (_.get(WIKI.config, 'mail.useDKIM', false)) {\n        conf = {\n          ...conf,\n          dkim: {\n            domainName: WIKI.config.mail.dkimDomainName,\n            keySelector: WIKI.config.mail.dkimKeySelector,\n            privateKey: WIKI.config.mail.dkimPrivateKey\n          }\n        }\n      }\n      this.transport = nodemailer.createTransport(conf)\n    } else {\n      WIKI.logger.warn('Mail is not setup! Please set the configuration in the administration area!')\n      this.transport = null\n    }\n    return this\n  },\n  async send(opts) {\n    if (!this.transport) {\n      WIKI.logger.warn('Cannot send email because mail is not setup in the administration area!')\n      throw new WIKI.Error.MailNotConfigured()\n    }\n    await this.loadTemplate(opts.template)\n    return this.transport.sendMail({\n      headers: {\n        'x-mailer': 'Wiki.js'\n      },\n      from: `\"${WIKI.config.mail.senderName}\" <${WIKI.config.mail.senderEmail}>`,\n      to: opts.to,\n      subject: `${opts.subject} - ${WIKI.config.title}`,\n      text: opts.text,\n      html: _.get(this.templates, opts.template)({\n        logo: (WIKI.config.logoUrl.startsWith('http') ? '' : WIKI.config.host) + WIKI.config.logoUrl,\n        siteTitle: WIKI.config.title,\n        copyright: WIKI.config.company.length > 0 ? WIKI.config.company : 'Powered by Wiki.js',\n        ...opts.data\n      })\n    })\n  },\n  async loadTemplate(key) {\n    if (_.has(this.templates, key)) { return }\n    const keyKebab = _.kebabCase(key)\n    try {\n      const rawTmpl = await fs.readFile(path.join(WIKI.SERVERPATH, `templates/${keyKebab}.html`), 'utf8')\n      _.set(this.templates, key, _.template(rawTmpl))\n    } catch (err) {\n      WIKI.logger.warn(err)\n      throw new WIKI.Error.MailTemplateFailed()\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/scheduler.js",
    "content": "const moment = require('moment')\nconst childProcess = require('child_process')\nconst _ = require('lodash')\nconst configHelper = require('../helpers/config')\n\n/* global WIKI */\n\nclass Job {\n  constructor({\n    name,\n    immediate = false,\n    schedule = 'P1D',\n    repeat = false,\n    worker = false\n  }, queue) {\n    this.queue = queue\n    this.finished = Promise.resolve()\n    this.name = name\n    this.immediate = immediate\n    this.schedule = moment.duration(schedule)\n    this.repeat = repeat\n    this.worker = worker\n  }\n\n  /**\n   * Start Job\n   *\n   * @param {Object} data Job Data\n   */\n  start(data) {\n    this.queue.jobs.push(this)\n    if (this.immediate) {\n      this.invoke(data)\n    } else {\n      this.enqueue(data)\n    }\n  }\n\n  /**\n   * Queue the next job run according to the wait duration\n   *\n   * @param {Object} data Job Data\n   */\n  enqueue(data) {\n    this.timeout = setTimeout(this.invoke.bind(this), this.schedule.asMilliseconds(), data)\n  }\n\n  /**\n   * Run the actual job\n   *\n   * @param {Object} data Job Data\n   */\n  async invoke(data) {\n    try {\n      if (this.worker) {\n        const proc = childProcess.fork(`server/core/worker.js`, [\n          `--job=${this.name}`,\n          `--data=${data}`\n        ], {\n          cwd: WIKI.ROOTPATH,\n          stdio: ['inherit', 'inherit', 'pipe', 'ipc']\n        })\n        const stderr = []\n        proc.stderr.on('data', chunk => stderr.push(chunk))\n        this.finished = new Promise((resolve, reject) => {\n          proc.on('exit', (code, signal) => {\n            const data = Buffer.concat(stderr).toString()\n            if (code === 0) {\n              resolve(data)\n            } else {\n              const err = new Error(`Error when running job ${this.name}: ${data}`)\n              err.exitSignal = signal\n              err.exitCode = code\n              err.stderr = data\n              reject(err)\n            }\n            proc.kill()\n          })\n        })\n      } else {\n        this.finished = require(`../jobs/${this.name}`)(data)\n      }\n      await this.finished\n    } catch (err) {\n      WIKI.logger.warn(err)\n    }\n    if (this.repeat && this.queue.jobs.includes(this)) {\n      this.enqueue(data)\n    } else {\n      this.stop().catch(() => {})\n    }\n  }\n\n  /**\n   * Stop any future job invocation from occuring\n   */\n  async stop() {\n    clearTimeout(this.timeout)\n    this.queue.jobs = this.queue.jobs.filter(x => x !== this)\n    return this.finished\n  }\n}\n\nmodule.exports = {\n  jobs: [],\n  init() {\n    return this\n  },\n  start() {\n    _.forOwn(WIKI.data.jobs, (queueParams, queueName) => {\n      if (WIKI.config.offline && queueParams.offlineSkip) {\n        WIKI.logger.warn(`Skipping job ${queueName} because offline mode is enabled. [SKIPPED]`)\n        return\n      }\n\n      const schedule = (configHelper.isValidDurationString(queueParams.schedule)) ? queueParams.schedule : 'P1D'\n      this.registerJob({\n        name: _.kebabCase(queueName),\n        immediate: _.get(queueParams, 'onInit', false),\n        schedule: schedule,\n        repeat: _.get(queueParams, 'repeat', false),\n        worker: _.get(queueParams, 'worker', false)\n      })\n    })\n  },\n  registerJob(opts, data) {\n    const job = new Job(opts, this)\n    job.start(data)\n    return job\n  },\n  async stop() {\n    return Promise.all(this.jobs.map(job => job.stop()))\n  }\n}\n"
  },
  {
    "path": "server/core/servers.js",
    "content": "const fs = require('fs-extra')\nconst http = require('http')\nconst https = require('https')\nconst { ApolloServer } = require('apollo-server-express')\nconst Promise = require('bluebird')\nconst _ = require('lodash')\nconst jwt = require('jsonwebtoken')\nconst cookie = require('cookie')\n\n/* global WIKI */\n\nmodule.exports = {\n  servers: {\n    graph: null,\n    http: null,\n    https: null\n  },\n  connections: new Map(),\n  le: null,\n  /**\n   * Start HTTP Server\n   */\n  async startHTTP () {\n    WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)\n    this.servers.http = http.createServer(WIKI.app)\n    this.servers.graph.installSubscriptionHandlers(this.servers.http)\n\n    this.servers.http.listen(WIKI.config.port, WIKI.config.bindIP)\n    this.servers.http.on('error', (error) => {\n      if (error.syscall !== 'listen') {\n        throw error\n      }\n\n      switch (error.code) {\n        case 'EACCES':\n          WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')\n          return process.exit(1)\n        case 'EADDRINUSE':\n          WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')\n          return process.exit(1)\n        default:\n          throw error\n      }\n    })\n\n    this.servers.http.on('listening', () => {\n      WIKI.logger.info('HTTP Server: [ RUNNING ]')\n    })\n\n    this.servers.http.on('connection', conn => {\n      let connKey = `http:${conn.remoteAddress}:${conn.remotePort}`\n      this.connections.set(connKey, conn)\n      conn.on('close', () => {\n        this.connections.delete(connKey)\n      })\n    })\n  },\n  /**\n   * Start HTTPS Server\n   */\n  async startHTTPS () {\n    if (WIKI.config.ssl.provider === 'letsencrypt') {\n      this.le = require('./letsencrypt')\n      await this.le.init()\n    }\n\n    WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.ssl.port} ]`)\n    const tlsOpts = {}\n    try {\n      if (WIKI.config.ssl.format === 'pem') {\n        tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : fs.readFileSync(WIKI.config.ssl.key)\n        tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : fs.readFileSync(WIKI.config.ssl.cert)\n      } else {\n        tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : fs.readFileSync(WIKI.config.ssl.pfx)\n      }\n      if (!_.isEmpty(WIKI.config.ssl.passphrase)) {\n        tlsOpts.passphrase = WIKI.config.ssl.passphrase\n      }\n      if (!_.isEmpty(WIKI.config.ssl.dhparam)) {\n        tlsOpts.dhparam = WIKI.config.ssl.dhparam\n      }\n    } catch (err) {\n      WIKI.logger.error('Failed to setup HTTPS server parameters:')\n      WIKI.logger.error(err)\n      return process.exit(1)\n    }\n    this.servers.https = https.createServer(tlsOpts, WIKI.app)\n    this.servers.graph.installSubscriptionHandlers(this.servers.https)\n\n    this.servers.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP)\n    this.servers.https.on('error', (error) => {\n      if (error.syscall !== 'listen') {\n        throw error\n      }\n\n      switch (error.code) {\n        case 'EACCES':\n          WIKI.logger.error('Listening on port ' + WIKI.config.ssl.port + ' requires elevated privileges!')\n          return process.exit(1)\n        case 'EADDRINUSE':\n          WIKI.logger.error('Port ' + WIKI.config.ssl.port + ' is already in use!')\n          return process.exit(1)\n        default:\n          throw error\n      }\n    })\n\n    this.servers.https.on('listening', () => {\n      WIKI.logger.info('HTTPS Server: [ RUNNING ]')\n    })\n\n    this.servers.https.on('connection', conn => {\n      let connKey = `https:${conn.remoteAddress}:${conn.remotePort}`\n      this.connections.set(connKey, conn)\n      conn.on('close', () => {\n        this.connections.delete(connKey)\n      })\n    })\n  },\n  /**\n   * Start GraphQL Server\n   */\n  async startGraphQL () {\n    const graphqlSchema = require('../graph')\n    this.servers.graph = new ApolloServer({\n      ...graphqlSchema,\n      context: ({ req, res }) => ({ req, res }),\n      subscriptions: {\n        onConnect: (connectionParams, webSocket) => {\n          let token = _.get(connectionParams, 'token', null)\n\n          if (!token) {\n            const cookieHeader = _.get(webSocket, 'upgradeReq.headers.cookie', '')\n            if (cookieHeader) {\n              const cookies = cookie.parse(cookieHeader)\n              token = cookies.jwt || null\n            }\n          }\n\n          if (!token) {\n            throw new Error('Unauthorized')\n          }\n\n          try {\n            const user = jwt.verify(token, WIKI.config.certs.public, {\n              audience: WIKI.config.auth.audience,\n              issuer: 'urn:wiki.js',\n              algorithms: ['RS256']\n            })\n\n            if (!_.includes(user.permissions, 'manage:system')) {\n              throw new Error('Forbidden')\n            }\n\n            return { user }\n          } catch (err) {\n            throw new Error('Unauthorized')\n          }\n        },\n        path: '/graphql-subscriptions'\n      }\n    })\n    this.servers.graph.applyMiddleware({ app: WIKI.app, cors: false })\n  },\n  /**\n   * Close all active connections\n   */\n  closeConnections (mode = 'all') {\n    for (const [key, conn] of this.connections) {\n      if (mode !== `all` && key.indexOf(`${mode}:`) !== 0) {\n        continue\n      }\n      conn.destroy()\n      this.connections.delete(key)\n    }\n    if (mode === 'all') {\n      this.connections.clear()\n    }\n  },\n  /**\n   * Stop all servers\n   */\n  async stopServers () {\n    this.closeConnections()\n    if (this.servers.http) {\n      await Promise.fromCallback(cb => { this.servers.http.close(cb) })\n      this.servers.http = null\n    }\n    if (this.servers.https) {\n      await Promise.fromCallback(cb => { this.servers.https.close(cb) })\n      this.servers.https = null\n    }\n    this.servers.graph = null\n  },\n  /**\n   * Restart Server\n   */\n  async restartServer (srv = 'https') {\n    this.closeConnections(srv)\n    switch (srv) {\n      case 'http':\n        if (this.servers.http) {\n          await Promise.fromCallback(cb => { this.servers.http.close(cb) })\n          this.servers.http = null\n        }\n        this.startHTTP()\n        break\n      case 'https':\n        if (this.servers.https) {\n          await Promise.fromCallback(cb => { this.servers.https.close(cb) })\n          this.servers.https = null\n        }\n        this.startHTTPS()\n        break\n      default:\n        throw new Error('Cannot restart server: Invalid designation')\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/sideloader.js",
    "content": "const fs = require('fs-extra')\nconst path = require('path')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = {\n  async init () {\n    if (!WIKI.config.offline) {\n      return\n    }\n\n    const sideloadExists = await fs.pathExists(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload'))\n\n    if (!sideloadExists) {\n      return\n    }\n\n    WIKI.logger.info('Sideload directory detected. Looking for packages...')\n\n    try {\n      await this.importLocales()\n    } catch (err) {\n      WIKI.logger.warn(err)\n    }\n  },\n  async importLocales() {\n    const localeExists = await fs.pathExists(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload/locales.json'))\n    if (localeExists) {\n      WIKI.logger.info('Found locales master file. Importing locale packages...')\n      let importedLocales = 0\n\n      const locales = await fs.readJson(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload/locales.json'))\n      if (locales && _.has(locales, 'data.localization.locales')) {\n        for (const locale of locales.data.localization.locales) {\n          try {\n            const localeData = await fs.readJson(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `sideload/${locale.code}.json`))\n            if (localeData) {\n              WIKI.logger.info(`Importing ${locale.name} locale package...`)\n\n              let lcObj = {}\n              _.forOwn(localeData, (value, key) => {\n                if (_.includes(key, '::')) { return }\n                if (_.isEmpty(value)) { value = key }\n                _.set(lcObj, key.replace(':', '.'), value)\n              })\n\n              const localeDbExists = await WIKI.models.locales.query().select('code').where('code', locale.code).first()\n              if (localeDbExists) {\n                await WIKI.models.locales.query().update({\n                  code: locale.code,\n                  strings: lcObj,\n                  isRTL: locale.isRTL,\n                  name: locale.name,\n                  nativeName: locale.nativeName,\n                  availability: locale.availability || 0\n                }).where('code', locale.code)\n              } else {\n                await WIKI.models.locales.query().insert({\n                  code: locale.code,\n                  strings: lcObj,\n                  isRTL: locale.isRTL,\n                  name: locale.name,\n                  nativeName: locale.nativeName,\n                  availability: locale.availability || 0\n                })\n              }\n              importedLocales++\n            }\n          } catch (err) {\n            // skip\n          }\n        }\n        WIKI.logger.info(`Imported ${importedLocales} locale packages: [COMPLETED]`)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/system.js",
    "content": "const _ = require('lodash')\nconst cfgHelper = require('../helpers/config')\nconst Promise = require('bluebird')\nconst fs = require('fs-extra')\nconst path = require('path')\nconst zlib = require('zlib')\nconst { pipeline } = require('node:stream/promises')\nconst { Readable, Transform } = require('node:stream')\n\n/* global WIKI */\n\nmodule.exports = {\n  updates: {\n    channel: 'BETA',\n    version: WIKI.version,\n    releaseDate: WIKI.releaseDate,\n    minimumVersionRequired: '2.0.0-beta.0',\n    minimumNodeRequired: '10.12.0'\n  },\n  exportStatus: {\n    status: 'notrunning',\n    progress: 0,\n    message: '',\n    updatedAt: null\n  },\n  init() {\n    // Clear content cache\n    fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))\n\n    return this\n  },\n  /**\n   * Upgrade from WIKI.js 1.x - MongoDB database\n   *\n   * @param {Object} opts Options object\n   */\n  async upgradeFromMongo (opts) {\n    WIKI.logger.info('Upgrading from MongoDB...')\n\n    let mongo = require('mongodb').MongoClient\n    let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr)\n\n    return new Promise((resolve, reject) => {\n      // Connect to MongoDB\n\n      mongo.connect(parsedMongoConStr, {\n        autoReconnect: false,\n        reconnectTries: 2,\n        reconnectInterval: 1000,\n        connectTimeoutMS: 5000,\n        socketTimeoutMS: 5000\n      }, async (err, db) => {\n        try {\n          if (err !== null) { throw err }\n\n          let users = db.collection('users')\n\n          // Check if users table is populated\n          let userCount = await users.count()\n          if (userCount < 2) {\n            throw new Error('MongoDB Upgrade: Users table is empty!')\n          }\n\n          // Import all users\n          let userData = await users.find({\n            email: {\n              $not: 'guest'\n            }\n          }).toArray()\n          await WIKI.models.User.bulkCreate(_.map(userData, usr => {\n            return {\n              email: usr.email,\n              name: usr.name || 'Imported User',\n              password: usr.password || '',\n              provider: usr.provider || 'local',\n              providerId: usr.providerId || '',\n              role: 'user',\n              createdAt: usr.createdAt\n            }\n          }))\n\n          resolve(true)\n        } catch (errc) {\n          reject(errc)\n        }\n        db.close()\n      })\n    })\n  },\n  /**\n   * Export Wiki to Disk\n   */\n  async export (opts) {\n    this.exportStatus.status = 'running'\n    this.exportStatus.progress = 0\n    this.exportStatus.message = ''\n    this.exportStatus.startedAt = new Date()\n\n    WIKI.logger.info(`Export started to path ${opts.path}`)\n    WIKI.logger.info(`Entities to export: ${opts.entities.join(', ')}`)\n\n    const progressMultiplier = 1 / opts.entities.length\n\n    try {\n      for (const entity of opts.entities) {\n        switch (entity) {\n          // -----------------------------------------\n          // ASSETS\n          // -----------------------------------------\n          case 'assets': {\n            WIKI.logger.info('Exporting assets...')\n            const assetFolders = await WIKI.models.assetFolders.getAllPaths()\n            const assetsCountRaw = await WIKI.models.assets.query().count('* as total').first()\n            const assetsCount = parseInt(assetsCountRaw.total)\n            if (assetsCount < 1) {\n              WIKI.logger.warn('There are no assets to export! Skipping...')\n              break\n            }\n            const assetsProgressMultiplier = progressMultiplier / Math.ceil(assetsCount / 50)\n            WIKI.logger.info(`Found ${assetsCount} assets to export. Streaming to disk...`)\n\n            await pipeline(\n              WIKI.models.knex.select('filename', 'folderId', 'data').from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),\n              new Transform({\n                objectMode: true,\n                transform: async (asset, enc, cb) => {\n                  const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename\n                  WIKI.logger.info(`Exporting asset ${filename}...`)\n                  await fs.outputFile(path.join(opts.path, 'assets', filename), asset.data)\n                  this.exportStatus.progress += assetsProgressMultiplier * 100\n                  cb()\n                }\n              })\n            )\n            WIKI.logger.info('Export: assets saved to disk successfully.')\n            break\n          }\n          // -----------------------------------------\n          // COMMENTS\n          // -----------------------------------------\n          case 'comments': {\n            WIKI.logger.info('Exporting comments...')\n            const outputPath = path.join(opts.path, 'comments.json.gz')\n            const commentsCountRaw = await WIKI.models.comments.query().count('* as total').first()\n            const commentsCount = parseInt(commentsCountRaw.total)\n            if (commentsCount < 1) {\n              WIKI.logger.warn('There are no comments to export! Skipping...')\n              break\n            }\n            const commentsProgressMultiplier = progressMultiplier / Math.ceil(commentsCount / 50)\n            WIKI.logger.info(`Found ${commentsCount} comments to export. Streaming to file...`)\n\n            const rs = Readable({ objectMode: true })\n            rs._read = () => {}\n\n            const fetchCommentsBatch = async (offset) => {\n              const comments = await WIKI.models.comments.query().offset(offset).limit(50).withGraphJoined({\n                author: true,\n                page: true\n              }).modifyGraph('author', builder => {\n                builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')\n              }).modifyGraph('page', builder => {\n                builder.select('pages.id', 'pages.path', 'pages.localeCode', 'pages.title')\n              })\n              if (comments.length > 0) {\n                for (const cmt of comments) {\n                  rs.push(cmt)\n                }\n                fetchCommentsBatch(offset + 50)\n              } else {\n                rs.push(null)\n              }\n              this.exportStatus.progress += commentsProgressMultiplier * 100\n            }\n            fetchCommentsBatch(0)\n\n            let marker = 0\n            await pipeline(\n              rs,\n              new Transform({\n                objectMode: true,\n                transform (chunk, encoding, callback) {\n                  marker++\n                  let outputStr = marker === 1 ? '[\\n' : ''\n                  outputStr += JSON.stringify(chunk, null, 2)\n                  if (marker < commentsCount) {\n                    outputStr += ',\\n'\n                  }\n                  callback(null, outputStr)\n                },\n                flush (callback) {\n                  callback(null, '\\n]')\n                }\n              }),\n              zlib.createGzip(),\n              fs.createWriteStream(outputPath)\n            )\n            WIKI.logger.info('Export: comments.json.gz created successfully.')\n            break\n          }\n          // -----------------------------------------\n          // GROUPS\n          // -----------------------------------------\n          case 'groups': {\n            WIKI.logger.info('Exporting groups...')\n            const outputPath = path.join(opts.path, 'groups.json')\n            const groups = await WIKI.models.groups.query()\n            await fs.outputJSON(outputPath, groups, { spaces: 2 })\n            WIKI.logger.info('Export: groups.json created successfully.')\n            this.exportStatus.progress += progressMultiplier * 100\n            break\n          }\n          // -----------------------------------------\n          // HISTORY\n          // -----------------------------------------\n          case 'history': {\n            WIKI.logger.info('Exporting pages history...')\n            const outputPath = path.join(opts.path, 'pages-history.json.gz')\n            const pagesCountRaw = await WIKI.models.pageHistory.query().count('* as total').first()\n            const pagesCount = parseInt(pagesCountRaw.total)\n            if (pagesCount < 1) {\n              WIKI.logger.warn('There are no pages history to export! Skipping...')\n              break\n            }\n            const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)\n            WIKI.logger.info(`Found ${pagesCount} pages history to export. Streaming to file...`)\n\n            const rs = Readable({ objectMode: true })\n            rs._read = () => {}\n\n            const fetchPagesBatch = async (offset) => {\n              const pages = await WIKI.models.pageHistory.query().offset(offset).limit(10).withGraphJoined({\n                author: true,\n                page: true,\n                tags: true\n              }).modifyGraph('author', builder => {\n                builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')\n              }).modifyGraph('page', builder => {\n                builder.select('pages.id', 'pages.title', 'pages.path', 'pages.localeCode')\n              }).modifyGraph('tags', builder => {\n                builder.select('tags.tag', 'tags.title')\n              })\n              if (pages.length > 0) {\n                for (const page of pages) {\n                  rs.push(page)\n                }\n                fetchPagesBatch(offset + 10)\n              } else {\n                rs.push(null)\n              }\n              this.exportStatus.progress += pagesProgressMultiplier * 100\n            }\n            fetchPagesBatch(0)\n\n            let marker = 0\n            await pipeline(\n              rs,\n              new Transform({\n                objectMode: true,\n                transform (chunk, encoding, callback) {\n                  marker++\n                  let outputStr = marker === 1 ? '[\\n' : ''\n                  outputStr += JSON.stringify(chunk, null, 2)\n                  if (marker < pagesCount) {\n                    outputStr += ',\\n'\n                  }\n                  callback(null, outputStr)\n                },\n                flush (callback) {\n                  callback(null, '\\n]')\n                }\n              }),\n              zlib.createGzip(),\n              fs.createWriteStream(outputPath)\n            )\n            WIKI.logger.info('Export: pages-history.json.gz created successfully.')\n            break\n          }\n          // -----------------------------------------\n          // NAVIGATION\n          // -----------------------------------------\n          case 'navigation': {\n            WIKI.logger.info('Exporting navigation...')\n            const outputPath = path.join(opts.path, 'navigation.json')\n            const navigationRaw = await WIKI.models.navigation.query()\n            const navigation = navigationRaw.reduce((obj, cur) => {\n              obj[cur.key] = cur.config\n              return obj\n            }, {})\n            await fs.outputJSON(outputPath, navigation, { spaces: 2 })\n            WIKI.logger.info('Export: navigation.json created successfully.')\n            this.exportStatus.progress += progressMultiplier * 100\n            break\n          }\n          // -----------------------------------------\n          // PAGES\n          // -----------------------------------------\n          case 'pages': {\n            WIKI.logger.info('Exporting pages...')\n            const outputPath = path.join(opts.path, 'pages.json.gz')\n            const pagesCountRaw = await WIKI.models.pages.query().count('* as total').first()\n            const pagesCount = parseInt(pagesCountRaw.total)\n            if (pagesCount < 1) {\n              WIKI.logger.warn('There are no pages to export! Skipping...')\n              break\n            }\n            const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)\n            WIKI.logger.info(`Found ${pagesCount} pages to export. Streaming to file...`)\n\n            const rs = Readable({ objectMode: true })\n            rs._read = () => {}\n\n            const fetchPagesBatch = async (offset) => {\n              const pages = await WIKI.models.pages.query().offset(offset).limit(10).withGraphJoined({\n                author: true,\n                creator: true,\n                tags: true\n              }).modifyGraph('author', builder => {\n                builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')\n              }).modifyGraph('creator', builder => {\n                builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')\n              }).modifyGraph('tags', builder => {\n                builder.select('tags.tag', 'tags.title')\n              })\n              if (pages.length > 0) {\n                for (const page of pages) {\n                  rs.push(page)\n                }\n                fetchPagesBatch(offset + 10)\n              } else {\n                rs.push(null)\n              }\n              this.exportStatus.progress += pagesProgressMultiplier * 100\n            }\n            fetchPagesBatch(0)\n\n            let marker = 0\n            await pipeline(\n              rs,\n              new Transform({\n                objectMode: true,\n                transform (chunk, encoding, callback) {\n                  marker++\n                  let outputStr = marker === 1 ? '[\\n' : ''\n                  outputStr += JSON.stringify(chunk, null, 2)\n                  if (marker < pagesCount) {\n                    outputStr += ',\\n'\n                  }\n                  callback(null, outputStr)\n                },\n                flush (callback) {\n                  callback(null, '\\n]')\n                }\n              }),\n              zlib.createGzip(),\n              fs.createWriteStream(outputPath)\n            )\n            WIKI.logger.info('Export: pages.json.gz created successfully.')\n            break\n          }\n          // -----------------------------------------\n          // SETTINGS\n          // -----------------------------------------\n          case 'settings': {\n            WIKI.logger.info('Exporting settings...')\n            const outputPath = path.join(opts.path, 'settings.json')\n            const config = {\n              ...WIKI.config,\n              modules: {\n                analytics: await WIKI.models.analytics.query(),\n                authentication: (await WIKI.models.authentication.query()).map(a => ({\n                  ...a,\n                  domainWhitelist: _.get(a, 'domainWhitelist.v', []),\n                  autoEnrollGroups: _.get(a, 'autoEnrollGroups.v', [])\n                })),\n                commentProviders: await WIKI.models.commentProviders.query(),\n                renderers: await WIKI.models.renderers.query(),\n                searchEngines: await WIKI.models.searchEngines.query(),\n                storage: await WIKI.models.storage.query()\n              },\n              apiKeys: await WIKI.models.apiKeys.query().where('isRevoked', false)\n            }\n            await fs.outputJSON(outputPath, config, { spaces: 2 })\n            WIKI.logger.info('Export: settings.json created successfully.')\n            this.exportStatus.progress += progressMultiplier * 100\n            break\n          }\n          // -----------------------------------------\n          // USERS\n          // -----------------------------------------\n          case 'users': {\n            WIKI.logger.info('Exporting users...')\n            const outputPath = path.join(opts.path, 'users.json.gz')\n            const usersCountRaw = await WIKI.models.users.query().count('* as total').first()\n            const usersCount = parseInt(usersCountRaw.total)\n            if (usersCount < 1) {\n              WIKI.logger.warn('There are no users to export! Skipping...')\n              break\n            }\n            const usersProgressMultiplier = progressMultiplier / Math.ceil(usersCount / 50)\n            WIKI.logger.info(`Found ${usersCount} users to export. Streaming to file...`)\n\n            const rs = Readable({ objectMode: true })\n            rs._read = () => {}\n\n            const fetchUsersBatch = async (offset) => {\n              const users = await WIKI.models.users.query().offset(offset).limit(50).withGraphJoined({\n                groups: true,\n                provider: true\n              }).modifyGraph('groups', builder => {\n                builder.select('groups.id', 'groups.name')\n              }).modifyGraph('provider', builder => {\n                builder.select('authentication.key', 'authentication.strategyKey', 'authentication.displayName')\n              })\n              if (users.length > 0) {\n                for (const usr of users) {\n                  rs.push(usr)\n                }\n                fetchUsersBatch(offset + 50)\n              } else {\n                rs.push(null)\n              }\n              this.exportStatus.progress += usersProgressMultiplier * 100\n            }\n            fetchUsersBatch(0)\n\n            let marker = 0\n            await pipeline(\n              rs,\n              new Transform({\n                objectMode: true,\n                transform (chunk, encoding, callback) {\n                  marker++\n                  let outputStr = marker === 1 ? '[\\n' : ''\n                  outputStr += JSON.stringify(chunk, null, 2)\n                  if (marker < usersCount) {\n                    outputStr += ',\\n'\n                  }\n                  callback(null, outputStr)\n                },\n                flush (callback) {\n                  callback(null, '\\n]')\n                }\n              }),\n              zlib.createGzip(),\n              fs.createWriteStream(outputPath)\n            )\n\n            WIKI.logger.info('Export: users.json.gz created successfully.')\n            break\n          }\n        }\n      }\n      this.exportStatus.status = 'success'\n      this.exportStatus.progress = 100\n    } catch (err) {\n      this.exportStatus.status = 'error'\n      this.exportStatus.message = err.message\n    }\n  }\n}\n"
  },
  {
    "path": "server/core/telemetry.js",
    "content": "const _ = require('lodash')\nconst { createApolloFetch } = require('apollo-fetch')\nconst { v4: uuid } = require('uuid')\nconst os = require('os')\nconst fs = require('fs-extra')\n\n/* global WIKI */\n\nmodule.exports = {\n  enabled: false,\n  init() {\n    WIKI.telemetry = this\n\n    if (_.get(WIKI.config, 'telemetry.isEnabled', false) === true && WIKI.config.offline !== true) {\n      this.enabled = true\n      this.sendInstanceEvent('STARTUP')\n    }\n  },\n  sendError(err) {\n    // TODO\n  },\n  sendEvent(eventCategory, eventAction, eventLabel) {\n    // TODO\n  },\n  async sendInstanceEvent(eventType) {\n    if (WIKI.devMode || !this.enabled) { return }\n\n    try {\n      const apollo = createApolloFetch({\n        uri: WIKI.config.graphEndpoint\n      })\n\n      // Platform detection\n      let platform = 'LINUX'\n      let isDockerized = false\n      let osname = `${os.type()} ${os.release()}`\n      switch (os.platform()) {\n        case 'win32':\n          platform = 'WINDOWS'\n          break\n        case 'darwin':\n          platform = 'MACOS'\n          break\n        default:\n          platform = 'LINUX'\n          isDockerized = await fs.pathExists('/.dockerenv')\n          if (isDockerized) {\n            osname = 'Docker'\n          }\n          break\n      }\n\n      // DB Version detection\n      let dbVersion = 'Unknown'\n      switch (WIKI.config.db.type) {\n        case 'mariadb':\n        case 'mysql':\n          const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;')\n          dbVersion = _.get(resultMYSQL, '[0][0].version', 'Unknown')\n          break\n        case 'mssql':\n          const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;')\n          dbVersion = _.get(resultMSSQL, '[0].version', 'Unknown')\n          break\n        case 'postgres':\n          dbVersion = _.get(WIKI.models, 'knex.client.version', 'Unknown')\n          break\n        case 'sqlite':\n          dbVersion = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown')\n          break\n      }\n\n      let arch = os.arch().toUpperCase()\n      if (['ARM', 'ARM64', 'X32', 'X64'].indexOf(arch) < 0) {\n        arch = 'OTHER'\n      }\n\n      // Send Event\n      const respStrings = await apollo({\n        query: `mutation (\n          $version: String!\n          $platform: TelemetryPlatform!\n          $os: String!\n          $architecture: TelemetryArchitecture!\n          $dbType: TelemetryDBType!\n          $dbVersion: String!\n          $nodeVersion: String!\n          $cpuCores: Int!\n          $ramMBytes: Int!,\n          $clientId: String!,\n          $event: TelemetryInstanceEvent!\n          ) {\n          telemetry {\n            instance(\n              version: $version\n              platform: $platform\n              os: $os\n              architecture: $architecture\n              dbType: $dbType\n              dbVersion: $dbVersion\n              nodeVersion: $nodeVersion\n              cpuCores: $cpuCores\n              ramMBytes: $ramMBytes\n              clientId: $clientId\n              event: $event\n            ) {\n              responseResult {\n                succeeded\n                errorCode\n                slug\n                message\n              }\n            }\n          }\n        }`,\n        variables: {\n          version: WIKI.version,\n          platform,\n          os: osname,\n          architecture: arch,\n          dbType: WIKI.config.db.type.toUpperCase(),\n          dbVersion,\n          nodeVersion: process.version.substr(1),\n          cpuCores: os.cpus().length,\n          ramMBytes: Math.round(os.totalmem() / 1024 / 1024),\n          clientId: WIKI.config.telemetry.clientId,\n          event: eventType\n        }\n      })\n      const telemetryResponse = _.get(respStrings, 'data.telemetry.instance.responseResult', { succeeded: false, message: 'Unexpected Error' })\n      if (!telemetryResponse.succeeded) {\n        WIKI.logger.warn('Failed to send instance telemetry: ' + telemetryResponse.message)\n      } else {\n        WIKI.logger.info('Telemetry is active: [ OK ]')\n      }\n    } catch (err) {\n      WIKI.logger.warn(err)\n    }\n  },\n  generateClientId() {\n    _.set(WIKI.config, 'telemetry.clientId', uuid())\n    return WIKI.config.telemetry.clientId\n  }\n}\n"
  },
  {
    "path": "server/core/worker.js",
    "content": "const path = require('path')\n\nlet WIKI = {\n  IS_DEBUG: process.env.NODE_ENV === 'development',\n  ROOTPATH: process.cwd(),\n  SERVERPATH: path.join(process.cwd(), 'server'),\n  Error: require('../helpers/error'),\n  configSvc: require('./config')\n}\nglobal.WIKI = WIKI\n\nWIKI.configSvc.init()\nWIKI.logger = require('./logger').init('JOB')\nconst args = require('yargs').argv\n\n;(async () => {\n  try {\n    await require(`../jobs/${args.job}`)(args.data)\n    process.exit(0)\n  } catch (e) {\n    await new Promise(resolve => process.stderr.write(e.message, resolve))\n    process.exit(1)\n  }\n})()\n"
  },
  {
    "path": "server/db/beta/index.js",
    "content": "const _ = require('lodash')\nconst path = require('path')\nconst fs = require('fs-extra')\nconst semver = require('semver')\n\n/* global WIKI */\n\nmodule.exports = {\n  async migrate (knex) {\n    const migrationsTableExists = await knex.schema.hasTable('migrations')\n    if (!migrationsTableExists) {\n      return\n    }\n\n    const dbCompat = {\n      charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n    }\n\n    const migrations = await knex('migrations')\n    if (_.some(migrations, m => m.name.indexOf('2.0.0-beta') >= 0)) {\n      // -> Pre-beta.241 locale field length fix\n      const localeColnInfo = await knex('pages').columnInfo('localeCode')\n      if (WIKI.config.db.type !== 'sqlite' && localeColnInfo.maxLength === 2) {\n        // -> Load locales\n        const locales = await knex('locales')\n        await knex.schema\n          // -> Remove constraints\n          .table('users', table => {\n            table.dropForeign('localeCode')\n          })\n          .table('pages', table => {\n            table.dropForeign('localeCode')\n          })\n          .table('pageHistory', table => {\n            table.dropForeign('localeCode')\n          })\n          .table('pageTree', table => {\n            table.dropForeign('localeCode')\n          })\n          // -> Recreate locales table\n          .dropTable('locales')\n          .createTable('locales', table => {\n            if (dbCompat.charset) { table.charset('utf8mb4') }\n            table.string('code', 5).notNullable().primary()\n            table.json('strings')\n            table.boolean('isRTL').notNullable().defaultTo(false)\n            table.string('name').notNullable()\n            table.string('nativeName').notNullable()\n            table.integer('availability').notNullable().defaultTo(0)\n            table.string('createdAt').notNullable()\n            table.string('updatedAt').notNullable()\n          })\n        await knex('locales').insert(locales)\n        // -> Alter columns length\n        await knex.schema\n          .table('users', table => {\n            table.string('localeCode', 5).notNullable().defaultTo('en').alter()\n          })\n          .table('pages', table => {\n            table.string('localeCode', 5).alter()\n          })\n          .table('pageHistory', table => {\n            table.string('localeCode', 5).alter()\n          })\n          .table('pageTree', table => {\n            table.string('localeCode', 5).alter()\n          })\n          // -> Restore restraints\n          .table('users', table => {\n            table.foreign('localeCode').references('code').inTable('locales')\n          })\n          .table('pages', table => {\n            table.foreign('localeCode').references('code').inTable('locales')\n          })\n          .table('pageHistory', table => {\n            table.foreign('localeCode').references('code').inTable('locales')\n          })\n          .table('pageTree', table => {\n            table.foreign('localeCode').references('code').inTable('locales')\n          })\n      }\n\n      // -> Advance to latest beta/rc migration state\n      const baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/beta/migrations' : 'db/beta/migrations-sqlite')\n      await knex.migrate.latest({\n        tableName: 'migrations',\n        migrationSource: {\n          async getMigrations() {\n            const migrationFiles = await fs.readdir(baseMigrationPath)\n            return migrationFiles.sort(semver.compare).map(m => ({\n              file: m,\n              directory: baseMigrationPath\n            }))\n          },\n          getMigrationName(migration) {\n            return migration.file\n          },\n          getMigration(migration) {\n            return require(path.join(baseMigrationPath, migration.file))\n          }\n        }\n      })\n\n      // -> Cleanup migration table\n      await knex('migrations').truncate()\n\n      // -> Advance to stable 2.0 migration state\n      await knex('migrations').insert({\n        name: '2.0.0.js',\n        batch: 1,\n        migration_time: knex.fn.now()\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.1.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    // =====================================\n    // MODEL TABLES\n    // =====================================\n    // ASSETS ------------------------------\n    .createTable('assets', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('filename').notNullable()\n      table.string('basename').notNullable()\n      table.string('ext').notNullable()\n      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')\n      table.string('mime').notNullable().defaultTo('application/octet-stream')\n      table.integer('fileSize').unsigned().comment('In kilobytes')\n      table.json('metadata')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // ASSET FOLDERS -----------------------\n    .createTable('assetFolders', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.string('slug').notNullable()\n      table.integer('parentId').unsigned().references('id').inTable('assetFolders')\n    })\n    // AUTHENTICATION ----------------------\n    .createTable('authentication', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n      table.boolean('selfRegistration').notNullable().defaultTo(false)\n      table.json('domainWhitelist').notNullable()\n      table.json('autoEnrollGroups').notNullable()\n    })\n    // COMMENTS ----------------------------\n    .createTable('comments', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.text('content').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // EDITORS -----------------------------\n    .createTable('editors', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n    // GROUPS ------------------------------\n    .createTable('groups', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.json('permissions').notNullable()\n      table.json('pageRules').notNullable()\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOCALES -----------------------------\n    .createTable('locales', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('code', 2).notNullable().primary()\n      table.json('strings')\n      table.boolean('isRTL').notNullable().defaultTo(false)\n      table.string('name').notNullable()\n      table.string('nativeName').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOGGING ----------------------------\n    .createTable('loggers', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('level').notNullable().defaultTo('warn')\n      table.json('config')\n    })\n    // NAVIGATION ----------------------------\n    .createTable('navigation', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.json('config')\n    })\n    // PAGE HISTORY ------------------------\n    .createTable('pageHistory', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n    })\n    // PAGES -------------------------------\n    .createTable('pages', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('privateNS')\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.text('render')\n      table.json('toc')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // PAGE TREE ---------------------------\n    .createTable('pageTree', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n    })\n    // RENDERERS ---------------------------\n    .createTable('renderers', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SEARCH ------------------------------\n    .createTable('searchEngines', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SETTINGS ----------------------------\n    .createTable('settings', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.json('value')\n      table.string('updatedAt').notNullable()\n    })\n    // STORAGE -----------------------------\n    .createTable('storage', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')\n      table.json('config')\n    })\n    // TAGS --------------------------------\n    .createTable('tags', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('tag').notNullable().unique()\n      table.string('title')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // USER KEYS ---------------------------\n    .createTable('userKeys', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('kind').notNullable()\n      table.string('token').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('validUntil').notNullable()\n    })\n    // USERS -------------------------------\n    .createTable('users', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('email').notNullable()\n      table.string('name').notNullable()\n      table.string('providerId')\n      table.string('password')\n      table.boolean('tfaIsActive').notNullable().defaultTo(false)\n      table.string('tfaSecret')\n      table.string('jobTitle').defaultTo('')\n      table.string('location').defaultTo('')\n      table.string('pictureUrl')\n      table.string('timezone').notNullable().defaultTo('America/New_York')\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.boolean('isActive').notNullable().defaultTo(false)\n      table.boolean('isVerified').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // =====================================\n    // RELATION TABLES\n    // =====================================\n    // PAGE HISTORY TAGS ---------------------------\n    .createTable('pageHistoryTags', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // PAGE TAGS ---------------------------\n    .createTable('pageTags', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // USER GROUPS -------------------------\n    .createTable('userGroups', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')\n      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')\n    })\n    // =====================================\n    // REFERENCES\n    // =====================================\n    .table('assets', table => {\n      table.integer('folderId').unsigned().references('id').inTable('assetFolders')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .table('comments', table => {\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .table('pageHistory', table => {\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 2).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .table('pages', table => {\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 2).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n      table.integer('creatorId').unsigned().references('id').inTable('users')\n    })\n    .table('pageTree', table => {\n      table.integer('parent').unsigned().references('id').inTable('pageTree')\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('localeCode', 2).references('code').inTable('locales')\n    })\n    .table('userKeys', table => {\n      table.integer('userId').unsigned().references('id').inTable('users')\n    })\n    .table('users', table => {\n      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')\n      table.string('localeCode', 2).references('code').inTable('locales').notNullable().defaultTo('en')\n      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')\n\n      table.unique(['providerKey', 'email'])\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('userGroups')\n    .dropTableIfExists('pageHistoryTags')\n    .dropTableIfExists('pageHistory')\n    .dropTableIfExists('pageTags')\n    .dropTableIfExists('assets')\n    .dropTableIfExists('assetFolders')\n    .dropTableIfExists('comments')\n    .dropTableIfExists('editors')\n    .dropTableIfExists('groups')\n    .dropTableIfExists('locales')\n    .dropTableIfExists('navigation')\n    .dropTableIfExists('pages')\n    .dropTableIfExists('renderers')\n    .dropTableIfExists('settings')\n    .dropTableIfExists('storage')\n    .dropTableIfExists('tags')\n    .dropTableIfExists('userKeys')\n    .dropTableIfExists('users')\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.11.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('pageHistory', table => {\n      table.string('action').defaultTo('updated')\n      table.dropForeign('pageId')\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('pageHistory', table => {\n      table.dropColumn('action')\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.127.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('assets', table => {\n      table.dropColumn('basename')\n      table.string('hash').notNullable()\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('assets', table => {\n      table.dropColumn('hash')\n      table.string('basename').notNullable()\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.148.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .table('assetData', table => {\n      if (dbCompat.blobLength) {\n        table.dropColumn('data')\n      }\n    })\n    .table('assetData', table => {\n      if (dbCompat.blobLength) {\n        table.specificType('data', 'LONGBLOB').notNullable()\n      }\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('assetData', table => {})\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.205.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .createTable('analytics', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('analytics')\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.217.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('locales', table => {\n      table.integer('availability').notNullable().defaultTo(0)\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('locales', table => {\n      table.dropColumn('availability')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.242.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('users', table => {\n      table.boolean('mustChangePwd').notNullable().defaultTo(false)\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('users', table => {\n      table.dropColumn('mustChangePwd')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.293.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .createTable('pageLinks', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('path').notNullable()\n      table.string('localeCode', 5).notNullable()\n    })\n    .table('pageLinks', table => {\n      table.index(['path', 'localeCode'])\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('pageLinks')\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.38.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('storage', table => {\n      table.string('syncInterval')\n      table.json('state')\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('storage', table => {\n      table.dropColumn('syncInterval')\n      table.dropColumn('state')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-beta.99.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .createTable('assetData', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.integer('id').primary()\n      table.binary('data').notNullable()\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('assetData')\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-rc.2.js",
    "content": "/* global WIKI */\n\nexports.up = async knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),\n    selfCascadeDelete: WIKI.config.db.type !== 'mssql'\n  }\n\n  return knex.schema\n    .dropTable('pageTree')\n    .createTable('pageTree', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.integer('id').unsigned().primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n    })\n    .table('pageTree', table => {\n      if (dbCompat.selfCascadeDelete) {\n        table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')\n      } else {\n        table.integer('parent').unsigned()\n      }\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n}\n\nexports.down = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),\n    selfCascadeDelete: WIKI.config.db.type !== 'mssql'\n  }\n  return knex.schema\n    .dropTable('pageTree')\n    .createTable('pageTree', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.integer('id').primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n    })\n    .table('pageTree', table => {\n      table.integer('parent').unsigned().references('id').inTable('pageTree')\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations/2.0.0-rc.29.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  return knex.schema\n    .table('pages', table => {\n      switch (WIKI.config.db.type) {\n        case 'mariadb':\n        case 'mysql':\n          table.specificType('content', 'LONGTEXT').alter()\n          table.specificType('render', 'LONGTEXT').alter()\n          break\n        case 'mssql':\n          table.specificType('content', 'VARCHAR(max)').alter()\n          table.specificType('render', 'VARCHAR(max)').alter()\n          break\n      }\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.1.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    // =====================================\n    // MODEL TABLES\n    // =====================================\n    // ASSETS ------------------------------\n    .createTable('assets', table => {\n      table.increments('id').primary()\n      table.string('filename').notNullable()\n      table.string('basename').notNullable()\n      table.string('ext').notNullable()\n      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')\n      table.string('mime').notNullable().defaultTo('application/octet-stream')\n      table.integer('fileSize').unsigned().comment('In kilobytes')\n      table.json('metadata')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.integer('folderId').unsigned().references('id').inTable('assetFolders')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    // ASSET FOLDERS -----------------------\n    .createTable('assetFolders', table => {\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.string('slug').notNullable()\n      table.integer('parentId').unsigned().references('id').inTable('assetFolders')\n    })\n    // AUTHENTICATION ----------------------\n    .createTable('authentication', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n      table.boolean('selfRegistration').notNullable().defaultTo(false)\n      table.json('domainWhitelist').notNullable()\n      table.json('autoEnrollGroups').notNullable()\n    })\n    // COMMENTS ----------------------------\n    .createTable('comments', table => {\n      table.increments('id').primary()\n      table.text('content').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    // EDITORS -----------------------------\n    .createTable('editors', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n    // GROUPS ------------------------------\n    .createTable('groups', table => {\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.json('permissions').notNullable()\n      table.json('pageRules').notNullable()\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOCALES -----------------------------\n    .createTable('locales', table => {\n      table.string('code', 5).notNullable().primary()\n      table.json('strings')\n      table.boolean('isRTL').notNullable().defaultTo(false)\n      table.string('name').notNullable()\n      table.string('nativeName').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOGGING ----------------------------\n    .createTable('loggers', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('level').notNullable().defaultTo('warn')\n      table.json('config')\n    })\n    // NAVIGATION ----------------------------\n    .createTable('navigation', table => {\n      table.string('key').notNullable().primary()\n      table.json('config')\n    })\n    // PAGE HISTORY ------------------------\n    .createTable('pageHistory', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    // PAGES -------------------------------\n    .createTable('pages', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('privateNS')\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.text('render')\n      table.json('toc')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n      table.integer('creatorId').unsigned().references('id').inTable('users')\n    })\n    // PAGE TREE ---------------------------\n    .createTable('pageTree', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n\n      table.integer('parent').unsigned().references('id').inTable('pageTree')\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n    // RENDERERS ---------------------------\n    .createTable('renderers', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SEARCH ------------------------------\n    .createTable('searchEngines', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SETTINGS ----------------------------\n    .createTable('settings', table => {\n      table.string('key').notNullable().primary()\n      table.json('value')\n      table.string('updatedAt').notNullable()\n    })\n    // STORAGE -----------------------------\n    .createTable('storage', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')\n      table.json('config')\n    })\n    // TAGS --------------------------------\n    .createTable('tags', table => {\n      table.increments('id').primary()\n      table.string('tag').notNullable().unique()\n      table.string('title')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // USER KEYS ---------------------------\n    .createTable('userKeys', table => {\n      table.increments('id').primary()\n      table.string('kind').notNullable()\n      table.string('token').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('validUntil').notNullable()\n\n      table.integer('userId').unsigned().references('id').inTable('users')\n    })\n    // USERS -------------------------------\n    .createTable('users', table => {\n      table.increments('id').primary()\n      table.string('email').notNullable()\n      table.string('name').notNullable()\n      table.string('providerId')\n      table.string('password')\n      table.boolean('tfaIsActive').notNullable().defaultTo(false)\n      table.string('tfaSecret')\n      table.string('jobTitle').defaultTo('')\n      table.string('location').defaultTo('')\n      table.string('pictureUrl')\n      table.string('timezone').notNullable().defaultTo('America/New_York')\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.boolean('isActive').notNullable().defaultTo(false)\n      table.boolean('isVerified').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')\n      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')\n      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')\n    })\n    // =====================================\n    // RELATION TABLES\n    // =====================================\n    // PAGE HISTORY TAGS ---------------------------\n    .createTable('pageHistoryTags', table => {\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // PAGE TAGS ---------------------------\n    .createTable('pageTags', table => {\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // USER GROUPS -------------------------\n    .createTable('userGroups', table => {\n      table.increments('id').primary()\n      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')\n      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')\n    })\n    // =====================================\n    // REFERENCES\n    // =====================================\n    .table('users', table => {\n      table.unique(['providerKey', 'email'])\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('userGroups')\n    .dropTableIfExists('pageHistoryTags')\n    .dropTableIfExists('pageHistory')\n    .dropTableIfExists('pageTags')\n    .dropTableIfExists('assets')\n    .dropTableIfExists('assetFolders')\n    .dropTableIfExists('comments')\n    .dropTableIfExists('editors')\n    .dropTableIfExists('groups')\n    .dropTableIfExists('locales')\n    .dropTableIfExists('navigation')\n    .dropTableIfExists('pages')\n    .dropTableIfExists('renderers')\n    .dropTableIfExists('settings')\n    .dropTableIfExists('storage')\n    .dropTableIfExists('tags')\n    .dropTableIfExists('userKeys')\n    .dropTableIfExists('users')\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.11.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .renameTable('pageHistory', 'pageHistory_old')\n    .createTable('pageHistory', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('action').defaultTo('updated')\n\n      table.integer('pageId').unsigned()\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .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;`)\n    .dropTable('pageHistory_old')\n}\n\nexports.down = knex => {\n  return knex.schema\n    .renameTable('pageHistory', 'pageHistory_old')\n    .createTable('pageHistory', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .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;')\n    .dropTable('pageHistory_old')\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.127.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('assets', table => {\n      table.dropColumn('basename')\n      table.string('hash').notNullable().defaultTo('')\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('assets', table => {\n      table.dropColumn('hash')\n      table.string('basename').notNullable().defaultTo('')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.205.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .createTable('analytics', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('analytics')\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.217.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('locales', table => {\n      table.integer('availability').notNullable().defaultTo(0)\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('locales', table => {\n      table.dropColumn('availability')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.242.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('users', table => {\n      table.boolean('mustChangePwd').notNullable().defaultTo(false)\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('users', table => {\n      table.dropColumn('mustChangePwd')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.293.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .createTable('pageLinks', table => {\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('path').notNullable()\n      table.string('localeCode', 5).notNullable()\n    })\n    .table('pageLinks', table => {\n      table.index(['path', 'localeCode'])\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('pageLinks')\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.38.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .table('storage', table => {\n      table.string('syncInterval')\n      table.json('state')\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .table('storage', table => {\n      table.dropColumn('syncInterval')\n      table.dropColumn('state')\n    })\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-beta.99.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .createTable('assetData', table => {\n      table.integer('id').primary()\n      table.binary('data').notNullable()\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTableIfExists('assetData')\n}\n"
  },
  {
    "path": "server/db/beta/migrations-sqlite/2.0.0-rc.2.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .dropTable('pageTree')\n    .createTable('pageTree', table => {\n      table.integer('id').primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n\n      table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n}\n\nexports.down = knex => {\n  return knex.schema\n    .dropTable('pageTree')\n    .createTable('pageTree', table => {\n      table.integer('id').primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n\n      table.integer('parent').unsigned().references('id').inTable('pageTree')\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n}\n"
  },
  {
    "path": "server/db/migrations/2.0.0.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),\n    selfCascadeDelete: WIKI.config.db.type !== 'mssql'\n  }\n  return knex.schema\n    // =====================================\n    // MODEL TABLES\n    // =====================================\n    // ANALYTICS ---------------------------\n    .createTable('analytics', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n    // ASSETS ------------------------------\n    .createTable('assets', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('filename').notNullable()\n      table.string('hash').notNullable()\n      table.string('ext').notNullable()\n      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')\n      table.string('mime').notNullable().defaultTo('application/octet-stream')\n      table.integer('fileSize').unsigned().comment('In kilobytes')\n      table.json('metadata')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // ASSET DATA --------------------------\n    .createTable('assetData', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.integer('id').primary()\n      if (dbCompat.blobLength) {\n        table.specificType('data', 'LONGBLOB').notNullable()\n      } else {\n        table.binary('data').notNullable()\n      }\n    })\n    // ASSET FOLDERS -----------------------\n    .createTable('assetFolders', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.string('slug').notNullable()\n      table.integer('parentId').unsigned().references('id').inTable('assetFolders')\n    })\n    // AUTHENTICATION ----------------------\n    .createTable('authentication', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n      table.boolean('selfRegistration').notNullable().defaultTo(false)\n      table.json('domainWhitelist').notNullable()\n      table.json('autoEnrollGroups').notNullable()\n    })\n    // COMMENTS ----------------------------\n    .createTable('comments', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.text('content').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // EDITORS -----------------------------\n    .createTable('editors', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n    // GROUPS ------------------------------\n    .createTable('groups', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.json('permissions').notNullable()\n      table.json('pageRules').notNullable()\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOCALES -----------------------------\n    .createTable('locales', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('code', 5).notNullable().primary()\n      table.json('strings')\n      table.boolean('isRTL').notNullable().defaultTo(false)\n      table.string('name').notNullable()\n      table.string('nativeName').notNullable()\n      table.integer('availability').notNullable().defaultTo(0)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOGGING ----------------------------\n    .createTable('loggers', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('level').notNullable().defaultTo('warn')\n      table.json('config')\n    })\n    // NAVIGATION ----------------------------\n    .createTable('navigation', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.json('config')\n    })\n    // PAGE HISTORY ------------------------\n    .createTable('pageHistory', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.string('action').defaultTo('updated')\n      table.integer('pageId').unsigned()\n      table.text('content')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n    })\n    // PAGE LINKS --------------------------\n    .createTable('pageLinks', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('localeCode', 5).notNullable()\n    })\n    // PAGES -------------------------------\n    .createTable('pages', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('privateNS')\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      switch (WIKI.config.db.type) {\n        case 'postgres':\n        case 'sqlite':\n          table.text('content')\n          table.text('render')\n          break\n        case 'mariadb':\n        case 'mysql':\n          table.specificType('content', 'LONGTEXT')\n          table.specificType('render', 'LONGTEXT')\n          break\n        case 'mssql':\n          table.specificType('content', 'VARCHAR(max)')\n          table.specificType('render', 'VARCHAR(max)')\n          break\n      }\n      table.json('toc')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // PAGE TREE ---------------------------\n    .createTable('pageTree', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.integer('id').unsigned().primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n    })\n    // RENDERERS ---------------------------\n    .createTable('renderers', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SEARCH ------------------------------\n    .createTable('searchEngines', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SETTINGS ----------------------------\n    .createTable('settings', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.json('value')\n      table.string('updatedAt').notNullable()\n    })\n    // STORAGE -----------------------------\n    .createTable('storage', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')\n      table.json('config')\n      table.string('syncInterval')\n      table.json('state')\n    })\n    // TAGS --------------------------------\n    .createTable('tags', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('tag').notNullable().unique()\n      table.string('title')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // USER KEYS ---------------------------\n    .createTable('userKeys', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('kind').notNullable()\n      table.string('token').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('validUntil').notNullable()\n    })\n    // USERS -------------------------------\n    .createTable('users', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('email').notNullable()\n      table.string('name').notNullable()\n      table.string('providerId')\n      table.string('password')\n      table.boolean('tfaIsActive').notNullable().defaultTo(false)\n      table.string('tfaSecret')\n      table.string('jobTitle').defaultTo('')\n      table.string('location').defaultTo('')\n      table.string('pictureUrl')\n      table.string('timezone').notNullable().defaultTo('America/New_York')\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.boolean('isActive').notNullable().defaultTo(false)\n      table.boolean('isVerified').notNullable().defaultTo(false)\n      table.boolean('mustChangePwd').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // =====================================\n    // RELATION TABLES\n    // =====================================\n    // PAGE HISTORY TAGS ---------------------------\n    .createTable('pageHistoryTags', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // PAGE TAGS ---------------------------\n    .createTable('pageTags', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // USER GROUPS -------------------------\n    .createTable('userGroups', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')\n      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')\n    })\n    // =====================================\n    // REFERENCES\n    // =====================================\n    .table('assets', table => {\n      table.integer('folderId').unsigned().references('id').inTable('assetFolders')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .table('comments', table => {\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .table('pageHistory', table => {\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    .table('pageLinks', table => {\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.index(['path', 'localeCode'])\n    })\n    .table('pages', table => {\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n      table.integer('creatorId').unsigned().references('id').inTable('users')\n    })\n    .table('pageTree', table => {\n      if (dbCompat.selfCascadeDelete) {\n        table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')\n      } else {\n        table.integer('parent').unsigned()\n      }\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n    .table('userKeys', table => {\n      table.integer('userId').unsigned().references('id').inTable('users')\n    })\n    .table('users', table => {\n      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')\n      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')\n      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')\n\n      table.unique(['providerKey', 'email'])\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.1.85.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  return knex.schema\n    .alterTable('pageHistory', table => {\n      switch (WIKI.config.db.type) {\n        // No change needed for PostgreSQL and SQLite\n        case 'mariadb':\n        case 'mysql':\n          table.specificType('content', 'LONGTEXT').alter()\n          break\n        case 'mssql':\n          table.specificType('content', 'VARCHAR(max)').alter()\n          break\n      }\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.2.17.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\nexports.up = async knex => {\n  let sqlVersionDate = ''\n  switch (WIKI.config.db.type) {\n    case 'postgres':\n      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\")'\n      break\n    case 'mssql':\n      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'\n      break\n    case 'mysql':\n    case 'mariadb':\n      // -> Fix for 2.2.50 failed migration\n      const pageHistoryColumns = await knex.schema.raw('SHOW COLUMNS FROM pageHistory')\n      if (_.some(pageHistoryColumns[0], ['Field', 'versionDate'])) {\n        console.info('MySQL 2.2.50 Migration Fix - Dropping failed versionDate column...')\n        await knex.schema.raw('ALTER TABLE pageHistory DROP COLUMN versionDate')\n        console.info('versionDate column dropped successfully.')\n      }\n\n      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`\n      break\n    // case 'mariadb':\n    //   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`\n    //   break\n  }\n  await knex.schema\n    .alterTable('pageHistory', table => {\n      table.string('versionDate').notNullable().defaultTo('')\n    })\n    .raw(sqlVersionDate)\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.2.3.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .createTable('apiKeys', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.text('key').notNullable()\n      table.string('expiration').notNullable()\n      table.boolean('isRevoked').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.3.10.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('users', table => {\n      table.string('lastLoginAt')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.3.23.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('pageTree', table => {\n      table.json('ancestors')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.4.13.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  return knex.schema\n    .alterTable('pages', table => {\n      if (WIKI.config.db.type === 'mysql') {\n        table.json('extra')\n      } else {\n        table.json('extra').notNullable().defaultTo('{}')\n      }\n    })\n    .alterTable('pageHistory', table => {\n      if (WIKI.config.db.type === 'mysql') {\n        table.json('extra')\n      } else {\n        table.json('extra').notNullable().defaultTo('{}')\n      }\n    })\n    .alterTable('users', table => {\n      table.string('dateFormat').notNullable().defaultTo('')\n      table.string('appearance').notNullable().defaultTo('')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.4.14.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .createTable('commentProviders', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.4.36.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('comments', table => {\n      table.text('render').notNullable().defaultTo('')\n      table.string('name').notNullable().defaultTo('')\n      table.string('email').notNullable().defaultTo('')\n      table.string('ip').notNullable().defaultTo('')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.4.61.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('comments', table => {\n      table.integer('replyTo').unsigned().notNullable().defaultTo(0)\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.5.1.js",
    "content": "exports.up = async knex => {\n  // Check for users using disabled strategies\n  let protectedStrategies = []\n  const disabledStrategies = await knex('authentication').where('isEnabled', false)\n  if (disabledStrategies) {\n    const incompatibleUsers = await knex('users').distinct('providerKey').whereIn('providerKey', disabledStrategies.map(s => s.key))\n    if (incompatibleUsers && incompatibleUsers.length > 0) {\n      protectedStrategies = incompatibleUsers.map(u => u.providerKey)\n    }\n  }\n\n  // Delete disabled strategies\n  await knex('authentication').whereNotIn('key', protectedStrategies).andWhere('isEnabled', false).del()\n\n  // Update table schema\n  await knex.schema\n    .alterTable('authentication', table => {\n      table.integer('order').unsigned().notNullable().defaultTo(0)\n      table.string('strategyKey').notNullable().defaultTo('')\n      table.string('displayName').notNullable().defaultTo('')\n    })\n\n  // Fix pre-2.5 strategies\n  const strategies = await knex('authentication')\n  let idx = 1\n  for (const strategy of strategies) {\n    await knex('authentication').where('key', strategy.key).update({\n      strategyKey: strategy.key,\n      order: (strategy.key === 'local') ? 0 : idx++\n    })\n  }\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.5.108.js",
    "content": "const has = require('lodash/has')\n\nexports.up = async knex => {\n  // -> Fix 2.5.1 added isEnabled columns for beta users\n  const localStrategy = await knex('authentication').where('key', 'local').first()\n  if (localStrategy && !has(localStrategy, 'isEnabled')) {\n    await knex.schema\n      .alterTable('authentication', table => {\n        table.boolean('isEnabled').notNullable().defaultTo(true)\n      })\n  }\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.5.118.js",
    "content": "exports.up = async knex => {\n  // -> Fix 2.5.117 new installations without isEnabled on local auth (#2382)\n  await knex('authentication').where('key', 'local').update({ isEnabled: true })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.5.12.js",
    "content": "exports.up = async knex => {\n  await knex.schema\n    .alterTable('groups', table => {\n      table.string('redirectOnLogin').notNullable().defaultTo('/')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.5.122.js",
    "content": "/* global WIKI */\n\nexports.up = knex => {\n  const dbCompat = {\n    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),\n    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)\n  }\n  return knex.schema\n    .createTable('userAvatars', table => {\n      if (dbCompat.charset) { table.charset('utf8mb4') }\n      table.integer('id').primary()\n      if (dbCompat.blobLength) {\n        table.specificType('data', 'LONGBLOB').notNullable()\n      } else {\n        table.binary('data').notNullable()\n      }\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations/2.5.128.js",
    "content": "exports.up = async knex => {\n  await knex('users').update({\n    email: knex.raw('LOWER(??)', ['email'])\n  })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.0.0.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    // =====================================\n    // MODEL TABLES\n    // =====================================\n    // ANALYTICS ---------------------------\n    .createTable('analytics', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n    // ASSETS ------------------------------\n    .createTable('assets', table => {\n      table.increments('id').primary()\n      table.string('filename').notNullable()\n      table.string('hash').notNullable().defaultTo('')\n      table.string('ext').notNullable()\n      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')\n      table.string('mime').notNullable().defaultTo('application/octet-stream')\n      table.integer('fileSize').unsigned().comment('In kilobytes')\n      table.json('metadata')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.integer('folderId').unsigned().references('id').inTable('assetFolders')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    // ASSET DATA --------------------------\n    .createTable('assetData', table => {\n      table.integer('id').primary()\n      table.binary('data').notNullable()\n    })\n    // ASSET FOLDERS -----------------------\n    .createTable('assetFolders', table => {\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.string('slug').notNullable()\n      table.integer('parentId').unsigned().references('id').inTable('assetFolders')\n    })\n    // AUTHENTICATION ----------------------\n    .createTable('authentication', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n      table.boolean('selfRegistration').notNullable().defaultTo(false)\n      table.json('domainWhitelist').notNullable()\n      table.json('autoEnrollGroups').notNullable()\n    })\n    // COMMENTS ----------------------------\n    .createTable('comments', table => {\n      table.increments('id').primary()\n      table.text('content').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.integer('pageId').unsigned().references('id').inTable('pages')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    // EDITORS -----------------------------\n    .createTable('editors', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n    // GROUPS ------------------------------\n    .createTable('groups', table => {\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.json('permissions').notNullable()\n      table.json('pageRules').notNullable()\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOCALES -----------------------------\n    .createTable('locales', table => {\n      table.string('code', 5).notNullable().primary()\n      table.json('strings')\n      table.boolean('isRTL').notNullable().defaultTo(false)\n      table.string('name').notNullable()\n      table.string('nativeName').notNullable()\n      table.integer('availability').notNullable().defaultTo(0)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // LOGGING ----------------------------\n    .createTable('loggers', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('level').notNullable().defaultTo('warn')\n      table.json('config')\n    })\n    // NAVIGATION ----------------------------\n    .createTable('navigation', table => {\n      table.string('key').notNullable().primary()\n      table.json('config')\n    })\n    // PAGE HISTORY ------------------------\n    .createTable('pageHistory', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('action').defaultTo('updated')\n\n      table.integer('pageId').unsigned()\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n    })\n    // PAGE LINKS --------------------------\n    .createTable('pageLinks', table => {\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('path').notNullable()\n      table.string('localeCode', 5).notNullable()\n    })\n    // PAGES -------------------------------\n    .createTable('pages', table => {\n      table.increments('id').primary()\n      table.string('path').notNullable()\n      table.string('hash').notNullable()\n      table.string('title').notNullable()\n      table.string('description')\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isPublished').notNullable().defaultTo(false)\n      table.string('privateNS')\n      table.string('publishStartDate')\n      table.string('publishEndDate')\n      table.text('content')\n      table.text('render')\n      table.json('toc')\n      table.string('contentType').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.string('editorKey').references('key').inTable('editors')\n      table.string('localeCode', 5).references('code').inTable('locales')\n      table.integer('authorId').unsigned().references('id').inTable('users')\n      table.integer('creatorId').unsigned().references('id').inTable('users')\n    })\n    // PAGE TREE ---------------------------\n    .createTable('pageTree', table => {\n      table.integer('id').primary()\n      table.string('path').notNullable()\n      table.integer('depth').unsigned().notNullable()\n      table.string('title').notNullable()\n      table.boolean('isPrivate').notNullable().defaultTo(false)\n      table.boolean('isFolder').notNullable().defaultTo(false)\n      table.string('privateNS')\n\n      table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.string('localeCode', 5).references('code').inTable('locales')\n    })\n    // RENDERERS ---------------------------\n    .createTable('renderers', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SEARCH ------------------------------\n    .createTable('searchEngines', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config')\n    })\n    // SETTINGS ----------------------------\n    .createTable('settings', table => {\n      table.string('key').notNullable().primary()\n      table.json('value')\n      table.string('updatedAt').notNullable()\n    })\n    // STORAGE -----------------------------\n    .createTable('storage', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')\n      table.json('config')\n      table.string('syncInterval')\n      table.json('state')\n    })\n    // TAGS --------------------------------\n    .createTable('tags', table => {\n      table.increments('id').primary()\n      table.string('tag').notNullable().unique()\n      table.string('title')\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n    // USER KEYS ---------------------------\n    .createTable('userKeys', table => {\n      table.increments('id').primary()\n      table.string('kind').notNullable()\n      table.string('token').notNullable()\n      table.string('createdAt').notNullable()\n      table.string('validUntil').notNullable()\n\n      table.integer('userId').unsigned().references('id').inTable('users')\n    })\n    // USERS -------------------------------\n    .createTable('users', table => {\n      table.increments('id').primary()\n      table.string('email').notNullable()\n      table.string('name').notNullable()\n      table.string('providerId')\n      table.string('password')\n      table.boolean('tfaIsActive').notNullable().defaultTo(false)\n      table.string('tfaSecret')\n      table.string('jobTitle').defaultTo('')\n      table.string('location').defaultTo('')\n      table.string('pictureUrl')\n      table.string('timezone').notNullable().defaultTo('America/New_York')\n      table.boolean('isSystem').notNullable().defaultTo(false)\n      table.boolean('isActive').notNullable().defaultTo(false)\n      table.boolean('isVerified').notNullable().defaultTo(false)\n      table.boolean('mustChangePwd').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n\n      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')\n      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')\n      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')\n    })\n    // =====================================\n    // RELATION TABLES\n    // =====================================\n    // PAGE HISTORY TAGS ---------------------------\n    .createTable('pageHistoryTags', table => {\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // PAGE TAGS ---------------------------\n    .createTable('pageTags', table => {\n      table.increments('id').primary()\n      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')\n      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')\n    })\n    // USER GROUPS -------------------------\n    .createTable('userGroups', table => {\n      table.increments('id').primary()\n      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')\n      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')\n    })\n    // =====================================\n    // REFERENCES\n    // =====================================\n    .table('users', table => {\n      table.unique(['providerKey', 'email'])\n    })\n    // =====================================\n    // INDEXES\n    // =====================================\n    .table('pageLinks', table => {\n      table.index(['path', 'localeCode'])\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.2.17.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('pageHistory', table => {\n      table.string('versionDate').notNullable().defaultTo('')\n    })\n    .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, '')`)\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.2.3.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .createTable('apiKeys', table => {\n      table.increments('id').primary()\n      table.string('name').notNullable()\n      table.text('key').notNullable()\n      table.string('expiration').notNullable()\n      table.boolean('isRevoked').notNullable().defaultTo(false)\n      table.string('createdAt').notNullable()\n      table.string('updatedAt').notNullable()\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.3.10.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('users', table => {\n      table.string('lastLoginAt')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.3.14.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .createTable('commentProviders', table => {\n      table.string('key').notNullable().primary()\n      table.boolean('isEnabled').notNullable().defaultTo(false)\n      table.json('config').notNullable()\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.3.23.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('pageTree', table => {\n      table.json('ancestors')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.4.13.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('pages', table => {\n      table.json('extra').notNullable().defaultTo('{}')\n    })\n    .alterTable('pageHistory', table => {\n      table.json('extra').notNullable().defaultTo('{}')\n    })\n    .alterTable('users', table => {\n      table.string('dateFormat').notNullable().defaultTo('')\n      table.string('appearance').notNullable().defaultTo('')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.4.36.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('comments', table => {\n      table.text('render').notNullable().defaultTo('')\n      table.string('name').notNullable().defaultTo('')\n      table.string('email').notNullable().defaultTo('')\n      table.string('ip').notNullable().defaultTo('')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.4.61.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .alterTable('comments', table => {\n      table.integer('replyTo').unsigned().notNullable().defaultTo(0)\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.5.1.js",
    "content": "exports.up = async knex => {\n  // Check for users using disabled strategies\n  let protectedStrategies = []\n  const disabledStrategies = await knex('authentication').where('isEnabled', false)\n  if (disabledStrategies) {\n    const incompatibleUsers = await knex('users').distinct('providerKey').whereIn('providerKey', disabledStrategies.map(s => s.key))\n    if (incompatibleUsers && incompatibleUsers.length > 0) {\n      protectedStrategies = incompatibleUsers.map(u => u.providerKey)\n    }\n  }\n\n  // Delete disabled strategies\n  await knex('authentication').whereNotIn('key', protectedStrategies).andWhere('isEnabled', false).del()\n\n  // Update table schema\n  await knex.schema\n    .alterTable('authentication', table => {\n      table.integer('order').unsigned().notNullable().defaultTo(0)\n      table.string('strategyKey').notNullable().defaultTo('')\n      table.string('displayName').notNullable().defaultTo('')\n    })\n\n  // Fix pre-2.5 strategies\n  const strategies = await knex('authentication')\n  let idx = 1\n  for (const strategy of strategies) {\n    await knex('authentication').where('key', strategy.key).update({\n      strategyKey: strategy.key,\n      order: (strategy.key === 'local') ? 0 : idx++\n    })\n  }\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.5.108.js",
    "content": "const has = require('lodash/has')\n\nexports.up = async knex => {\n  // -> Fix 2.5.1 added isEnabled columns for beta users\n  const localStrategy = await knex('authentication').where('key', 'local').first()\n  if (localStrategy && !has(localStrategy, 'isEnabled')) {\n    await knex.schema\n      .alterTable('authentication', table => {\n        table.boolean('isEnabled').notNullable().defaultTo(true)\n      })\n  }\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.5.118.js",
    "content": "exports.up = async knex => {\n  // -> Fix 2.5.117 new installations without isEnabled on local auth (#2382)\n  await knex('authentication').where('key', 'local').update({ isEnabled: true })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.5.12.js",
    "content": "exports.up = async knex => {\n  await knex.schema\n    .alterTable('groups', table => {\n      table.string('redirectOnLogin').notNullable().defaultTo('/')\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.5.122.js",
    "content": "exports.up = knex => {\n  return knex.schema\n    .createTable('userAvatars', table => {\n      table.integer('id').primary()\n      table.binary('data').notNullable()\n    })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrations-sqlite/2.5.128.js",
    "content": "exports.up = async knex => {\n  await knex('users').update({\n    email: knex.raw('LOWER(email)')\n  })\n}\n\nexports.down = knex => { }\n"
  },
  {
    "path": "server/db/migrator-source.js",
    "content": "const path = require('path')\nconst fs = require('fs-extra')\nconst semver = require('semver')\n\nconst baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/migrations' : 'db/migrations-sqlite')\n\n/* global WIKI */\n\nmodule.exports = {\n  /**\n   * Gets the migration names\n   * @returns Promise<string[]>\n   */\n  async getMigrations() {\n    const migrationFiles = await fs.readdir(baseMigrationPath)\n    return migrationFiles.map(m => m.replace('.js', '')).sort(semver.compare).map(m => ({\n      file: m,\n      directory: baseMigrationPath\n    }))\n  },\n\n  getMigrationName(migration) {\n    return migration.file.indexOf('.js') >= 0 ? migration.file : `${migration.file}.js`\n  },\n\n  getMigration(migration) {\n    return require(path.join(baseMigrationPath, migration.file))\n  }\n}\n"
  },
  {
    "path": "server/graph/directives/auth.js",
    "content": "const { SchemaDirectiveVisitor } = require('graphql-tools')\nconst { defaultFieldResolver } = require('graphql')\nconst _ = require('lodash')\n\nclass AuthDirective extends SchemaDirectiveVisitor {\n  visitObject(type) {\n    this.ensureFieldsWrapped(type)\n    type._requiredAuthScopes = this.args.requires\n  }\n  // Visitor methods for nested types like fields and arguments\n  // also receive a details object that provides information about\n  // the parent and grandparent types.\n  visitFieldDefinition(field, details) {\n    this.ensureFieldsWrapped(details.objectType)\n    field._requiredAuthScopes = this.args.requires\n  }\n\n  visitArgumentDefinition(argument, details) {\n    this.ensureFieldsWrapped(details.objectType)\n    argument._requiredAuthScopes = this.args.requires\n  }\n\n  ensureFieldsWrapped(objectType) {\n    // Mark the GraphQLObjectType object to avoid re-wrapping:\n    if (objectType._authFieldsWrapped) return\n    objectType._authFieldsWrapped = true\n\n    const fields = objectType.getFields()\n\n    Object.keys(fields).forEach(fieldName => {\n      const field = fields[fieldName]\n      const { resolve = defaultFieldResolver } = field\n      field.resolve = async function (...args) {\n        // Get the required scopes from the field first, falling back\n        // to the objectType if no scopes is required by the field:\n        const requiredScopes = field._requiredAuthScopes || objectType._requiredAuthScopes\n\n        if (!requiredScopes) {\n          return resolve.apply(this, args)\n        }\n\n        const context = args[2]\n        if (!context.req.user) {\n          throw new Error('Unauthorized')\n        }\n        if (!_.some(context.req.user.permissions, pm => _.includes(requiredScopes, pm))) {\n          throw new Error('Forbidden')\n        }\n\n        return resolve.apply(this, args)\n      }\n    })\n  }\n}\n\nmodule.exports = AuthDirective\n"
  },
  {
    "path": "server/graph/directives/rate-limit.js",
    "content": "const { createRateLimitDirective } = require('graphql-rate-limit-directive')\n\nmodule.exports = createRateLimitDirective({\n  keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${info.parentType}.${info.fieldName}`\n})\n"
  },
  {
    "path": "server/graph/index.js",
    "content": "const _ = require('lodash')\nconst fs = require('fs')\n// const gqlTools = require('graphql-tools')\nconst path = require('path')\nconst autoload = require('auto-load')\nconst PubSub = require('graphql-subscriptions').PubSub\nconst { LEVEL, MESSAGE } = require('triple-beam')\nconst Transport = require('winston-transport')\nconst { createRateLimitTypeDef } = require('graphql-rate-limit-directive')\n// const { GraphQLUpload } = require('graphql-upload')\n\n/* global WIKI */\n\nWIKI.logger.info(`Loading GraphQL Schema...`)\n\n// Init Subscription PubSub\n\nWIKI.GQLEmitter = new PubSub()\n\n// Schemas\n\nlet typeDefs = [createRateLimitTypeDef()]\nlet schemas = fs.readdirSync(path.join(WIKI.SERVERPATH, 'graph/schemas'))\nschemas.forEach(schema => {\n  typeDefs.push(fs.readFileSync(path.join(WIKI.SERVERPATH, `graph/schemas/${schema}`), 'utf8'))\n})\n\n// Resolvers\n\nlet resolvers = {\n  // Upload: GraphQLUpload\n}\nconst resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))\nresolversObj.forEach(resolver => {\n  _.merge(resolvers, resolver)\n})\n\n// Directives\n\nlet schemaDirectives = {\n  ...autoload(path.join(WIKI.SERVERPATH, 'graph/directives'))\n}\n\n// Live Trail Logger (admin)\n\nclass LiveTrailLogger extends Transport {\n  constructor(opts) {\n    super(opts)\n\n    this.name = 'liveTrailLogger'\n    this.level = 'debug'\n  }\n\n  log (info, callback = () => {}) {\n    WIKI.GQLEmitter.publish('livetrail', {\n      loggingLiveTrail: {\n        timestamp: new Date(),\n        level: info[LEVEL],\n        output: info[MESSAGE]\n      }\n    })\n    callback(null, true)\n  }\n}\n\nWIKI.logger.add(new LiveTrailLogger({}))\n\nWIKI.logger.info(`GraphQL Schema: [ OK ]`)\n\nmodule.exports = {\n  typeDefs,\n  resolvers,\n  schemaDirectives\n}\n"
  },
  {
    "path": "server/graph/resolvers/analytics.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async analytics() { return {} }\n  },\n  Mutation: {\n    async analytics() { return {} }\n  },\n  AnalyticsQuery: {\n    async providers(obj, args, context, info) {\n      let providers = await WIKI.models.analytics.getProviders(args.isEnabled)\n      providers = providers.map(stg => {\n        const providerInfo = _.find(WIKI.data.analytics, ['key', stg.key]) || {}\n        return {\n          ...providerInfo,\n          ...stg,\n          config: _.sortBy(_.transform(stg.config, (res, value, key) => {\n            const configData = _.get(providerInfo.props, key, {})\n            res.push({\n              key,\n              value: JSON.stringify({\n                ...configData,\n                value\n              })\n            })\n          }, []), 'key')\n        }\n      })\n      return providers\n    }\n  },\n  AnalyticsMutation: {\n    async updateProviders(obj, args, context) {\n      try {\n        for (let str of args.providers) {\n          await WIKI.models.analytics.query().patch({\n            isEnabled: str.isEnabled,\n            config: _.reduce(str.config, (result, value, key) => {\n              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))\n              return result\n            }, {})\n          }).where('key', str.key)\n          await WIKI.cache.del('analytics')\n        }\n        return {\n          responseResult: graphHelper.generateSuccess('Providers updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/asset.js",
    "content": "const _ = require('lodash')\nconst sanitize = require('sanitize-filename')\nconst graphHelper = require('../../helpers/graph')\nconst assetHelper = require('../../helpers/asset')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async assets() { return {} }\n  },\n  Mutation: {\n    async assets() { return {} }\n  },\n  AssetQuery: {\n    async list(obj, args, context) {\n      let cond = {\n        folderId: args.folderId === 0 ? null : args.folderId\n      }\n      if (args.kind !== 'ALL') {\n        cond.kind = args.kind.toLowerCase()\n      }\n      const folderHierarchy = await WIKI.models.assetFolders.getHierarchy(args.folderId)\n      const folderPath = folderHierarchy.map(h => h.slug).join('/')\n      const results = await WIKI.models.assets.query().where(cond)\n      return _.filter(results, r => {\n        const path = folderPath ? `${folderPath}/${r.filename}` : r.filename\n        return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path })\n      }).map(a => ({\n        ...a,\n        kind: a.kind.toUpperCase()\n      }))\n    },\n    async folders(obj, args, context) {\n      const results = await WIKI.models.assetFolders.query().where({\n        parentId: args.parentFolderId === 0 ? null : args.parentFolderId\n      })\n      const parentHierarchy = await WIKI.models.assetFolders.getHierarchy(args.parentFolderId)\n      const parentPath = parentHierarchy.map(h => h.slug).join('/')\n      return _.filter(results, r => {\n        const path = parentPath ? `${parentPath}/${r.slug}` : r.slug\n        return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path })\n      })\n    }\n  },\n  AssetMutation: {\n    /**\n     * Create New Asset Folder\n     */\n    async createFolder(obj, args, context) {\n      try {\n        const folderSlug = sanitize(args.slug).toLowerCase()\n        const parentFolderId = args.parentFolderId === 0 ? null : args.parentFolderId\n        const result = await WIKI.models.assetFolders.query().where({\n          parentId: parentFolderId,\n          slug: folderSlug\n        }).first()\n        if (!result) {\n          await WIKI.models.assetFolders.query().insert({\n            slug: folderSlug,\n            name: folderSlug,\n            parentId: parentFolderId\n          })\n          return {\n            responseResult: graphHelper.generateSuccess('Asset Folder has been created successfully.')\n          }\n        } else {\n          throw new WIKI.Error.AssetFolderExists()\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Rename an Asset\n     */\n    async renameAsset(obj, args, context) {\n      try {\n        const filename = sanitize(args.filename).toLowerCase()\n\n        const asset = await WIKI.models.assets.query().findById(args.id)\n        if (asset) {\n          // Check for extension mismatch\n          if (!_.endsWith(filename, asset.ext)) {\n            throw new WIKI.Error.AssetRenameInvalidExt()\n          }\n\n          // Check for non-dot files changing to dotfile\n          if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) {\n            throw new WIKI.Error.AssetRenameInvalid()\n          }\n\n          // Check for collision\n          const assetCollision = await WIKI.models.assets.query().where({\n            filename,\n            folderId: asset.folderId\n          }).first()\n          if (assetCollision) {\n            throw new WIKI.Error.AssetRenameCollision()\n          }\n\n          // Get asset folder path\n          let hierarchy = []\n          if (asset.folderId) {\n            hierarchy = await WIKI.models.assetFolders.getHierarchy(asset.folderId)\n          }\n\n          // Check source asset permissions\n          const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename\n          if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {\n            throw new WIKI.Error.AssetRenameForbidden()\n          }\n\n          // Check target asset permissions\n          const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename\n          if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {\n            throw new WIKI.Error.AssetRenameTargetForbidden()\n          }\n\n          // Update filename + hash\n          const fileHash = assetHelper.generateHash(assetTargetPath)\n          await WIKI.models.assets.query().patch({\n            filename: filename,\n            hash: fileHash\n          }).findById(args.id)\n\n          // Delete old asset cache\n          await asset.deleteAssetCache()\n\n          // Rename in Storage\n          await WIKI.models.storage.assetEvent({\n            event: 'renamed',\n            asset: {\n              ...asset,\n              path: assetSourcePath,\n              destinationPath: assetTargetPath,\n              moveAuthorId: context.req.user.id,\n              moveAuthorName: context.req.user.name,\n              moveAuthorEmail: context.req.user.email\n            }\n          })\n\n          return {\n            responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.')\n          }\n        } else {\n          throw new WIKI.Error.AssetInvalid()\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Delete an Asset\n     */\n    async deleteAsset(obj, args, context) {\n      try {\n        const asset = await WIKI.models.assets.query().findById(args.id)\n        if (asset) {\n          // Check permissions\n          const assetPath = await asset.getAssetPath()\n          if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {\n            throw new WIKI.Error.AssetDeleteForbidden()\n          }\n\n          await WIKI.models.knex('assetData').where('id', args.id).del()\n          await WIKI.models.assets.query().deleteById(args.id)\n          await asset.deleteAssetCache()\n\n          // Delete from Storage\n          await WIKI.models.storage.assetEvent({\n            event: 'deleted',\n            asset: {\n              ...asset,\n              path: assetPath,\n              authorId: context.req.user.id,\n              authorName: context.req.user.name,\n              authorEmail: context.req.user.email\n            }\n          })\n\n          return {\n            responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.')\n          }\n        } else {\n          throw new WIKI.Error.AssetInvalid()\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Flush Temporary Uploads\n     */\n    async flushTempUploads(obj, args, context) {\n      try {\n        await WIKI.models.assets.flushTempUploads()\n        return {\n          responseResult: graphHelper.generateSuccess('Temporary Uploads have been flushed successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n  // File: {\n  //   folder(fl) {\n  //     return fl.getFolder()\n  //   }\n  // }\n}\n"
  },
  {
    "path": "server/graph/resolvers/authentication.js",
    "content": "const _ = require('lodash')\nconst fs = require('fs-extra')\nconst path = require('path')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async authentication () { return {} }\n  },\n  Mutation: {\n    async authentication () { return {} }\n  },\n  AuthenticationQuery: {\n    /**\n     * List of API Keys\n     */\n    async apiKeys (obj, args, context) {\n      const keys = await WIKI.models.apiKeys.query().orderBy(['isRevoked', 'name'])\n      return keys.map(k => ({\n        id: k.id,\n        name: k.name,\n        keyShort: '...' + k.key.substring(k.key.length - 20),\n        isRevoked: k.isRevoked,\n        expiration: k.expiration,\n        createdAt: k.createdAt,\n        updatedAt: k.updatedAt\n      }))\n    },\n    /**\n     * Current API State\n     */\n    apiState () {\n      return WIKI.config.api.isEnabled\n    },\n    async strategies () {\n      return WIKI.data.authentication.map(stg => ({\n        ...stg,\n        isAvailable: stg.isAvailable === true,\n        props: _.sortBy(_.transform(stg.props, (res, value, key) => {\n          res.push({\n            key,\n            value: JSON.stringify(value)\n          })\n        }, []), 'key')\n      }))\n    },\n    /**\n     * Fetch active authentication strategies\n     */\n    async activeStrategies (obj, args, context, info) {\n      let strategies = await WIKI.models.authentication.getStrategies()\n      strategies = strategies.map(stg => {\n        const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}\n        return {\n          ...stg,\n          strategy: strategyInfo,\n          config: _.sortBy(_.transform(stg.config, (res, value, key) => {\n            const configData = _.get(strategyInfo.props, key, false)\n            if (configData) {\n              res.push({\n                key,\n                value: JSON.stringify({\n                  ...configData,\n                  value\n                })\n              })\n            }\n          }, []), 'key')\n        }\n      })\n      return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies\n    }\n  },\n  AuthenticationMutation: {\n    /**\n     * Create New API Key\n     */\n    async createApiKey (obj, args, context) {\n      try {\n        const key = await WIKI.models.apiKeys.createNewKey(args)\n        await WIKI.auth.reloadApiKeys()\n        WIKI.events.outbound.emit('reloadApiKeys')\n        return {\n          key,\n          responseResult: graphHelper.generateSuccess('API Key created successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Perform Login\n     */\n    async login (obj, args, context) {\n      try {\n        const authResult = await WIKI.models.users.login(args, context)\n        return {\n          ...authResult,\n          responseResult: graphHelper.generateSuccess('Login success')\n        }\n      } catch (err) {\n        // LDAP Debug Flag\n        if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) {\n          WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err)\n        }\n\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Perform 2FA Login\n     */\n    async loginTFA (obj, args, context) {\n      try {\n        const authResult = await WIKI.models.users.loginTFA(args, context)\n        return {\n          ...authResult,\n          responseResult: graphHelper.generateSuccess('TFA success')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Perform Mandatory Password Change after Login\n     */\n    async loginChangePassword (obj, args, context) {\n      try {\n        const authResult = await WIKI.models.users.loginChangePassword(args, context)\n        return {\n          ...authResult,\n          responseResult: graphHelper.generateSuccess('Password changed successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Perform Mandatory Password Change after Login\n     */\n    async forgotPassword (obj, args, context) {\n      try {\n        await WIKI.models.users.loginForgotPassword(args, context)\n        return {\n          responseResult: graphHelper.generateSuccess('Password reset request processed.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Register a new account\n     */\n    async register (obj, args, context) {\n      try {\n        await WIKI.models.users.register({ ...args, verify: true }, context)\n        return {\n          responseResult: graphHelper.generateSuccess('Registration success')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Set API state\n     */\n    async setApiState (obj, args, context) {\n      try {\n        WIKI.config.api.isEnabled = args.enabled\n        await WIKI.configSvc.saveToDb(['api'])\n        return {\n          responseResult: graphHelper.generateSuccess('API State changed successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Revoke an API key\n     */\n    async revokeApiKey (obj, args, context) {\n      try {\n        await WIKI.models.apiKeys.query().findById(args.id).patch({\n          isRevoked: true\n        })\n        await WIKI.auth.reloadApiKeys()\n        WIKI.events.outbound.emit('reloadApiKeys')\n        return {\n          responseResult: graphHelper.generateSuccess('API Key revoked successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Update Authentication Strategies\n     */\n    async updateStrategies (obj, args, context) {\n      try {\n        const previousStrategies = await WIKI.models.authentication.getStrategies()\n        for (const str of args.strategies) {\n          const newStr = {\n            displayName: str.displayName,\n            order: str.order,\n            isEnabled: str.isEnabled,\n            config: _.reduce(str.config, (result, value, key) => {\n              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))\n              return result\n            }, {}),\n            selfRegistration: str.selfRegistration,\n            domainWhitelist: { v: str.domainWhitelist },\n            autoEnrollGroups: { v: str.autoEnrollGroups }\n          }\n\n          if (_.some(previousStrategies, ['key', str.key])) {\n            await WIKI.models.authentication.query().patch({\n              key: str.key,\n              strategyKey: str.strategyKey,\n              ...newStr\n            }).where('key', str.key)\n          } else {\n            await WIKI.models.authentication.query().insert({\n              key: str.key,\n              strategyKey: str.strategyKey,\n              ...newStr\n            })\n          }\n        }\n\n        for (const str of _.differenceBy(previousStrategies, args.strategies, 'key')) {\n          const hasUsers = await WIKI.models.users.query().count('* as total').where({ providerKey: str.key }).first()\n          if (_.toSafeInteger(hasUsers.total) > 0) {\n            throw new Error(`Cannot delete ${str.displayName} as 1 or more users are still using it.`)\n          } else {\n            await WIKI.models.authentication.query().delete().where('key', str.key)\n          }\n        }\n\n        await WIKI.auth.activateStrategies()\n        WIKI.events.outbound.emit('reloadAuthStrategies')\n        return {\n          responseResult: graphHelper.generateSuccess('Strategies updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Generate New Authentication Public / Private Key Certificates\n     */\n    async regenerateCertificates (obj, args, context) {\n      try {\n        await WIKI.auth.regenerateCertificates()\n        return {\n          responseResult: graphHelper.generateSuccess('Certificates have been regenerated successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Reset Guest User\n     */\n    async resetGuestUser (obj, args, context) {\n      try {\n        await WIKI.auth.resetGuestUser()\n        return {\n          responseResult: graphHelper.generateSuccess('Guest user has been reset successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  },\n  AuthenticationStrategy: {\n    icon (ap, args) {\n      return fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${ap.key}.svg`), 'utf8').catch(err => {\n        if (err.code === 'ENOENT') {\n          return null\n        }\n        throw err\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/comment.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async comments() { return {} }\n  },\n  Mutation: {\n    async comments() { return {} }\n  },\n  CommentQuery: {\n    /**\n     * Fetch list of Comments Providers\n     */\n    async providers(obj, args, context, info) {\n      const providers = await WIKI.models.commentProviders.getProviders()\n      return providers.map(provider => {\n        const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {}\n        return {\n          ...providerInfo,\n          ...provider,\n          config: _.sortBy(_.transform(provider.config, (res, value, key) => {\n            const configData = _.get(providerInfo.props, key, false)\n            if (configData) {\n              res.push({\n                key,\n                value: JSON.stringify({\n                  ...configData,\n                  value\n                })\n              })\n            }\n          }, []), 'key')\n        }\n      })\n    },\n    /**\n     * Fetch list of comments for a page\n     */\n    async list (obj, args, context) {\n      const page = await WIKI.models.pages.query().select('pages.id').findOne({ localeCode: args.locale, path: args.path })\n        .withGraphJoined('tags')\n        .modifyGraph('tags', builder => {\n          builder.select('tag')\n        })\n      if (page) {\n        if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], { tags: page.tags, ...args })) {\n          const comments = await WIKI.models.comments.query().where('pageId', page.id).orderBy('createdAt')\n          return comments.map(c => ({\n            ...c,\n            authorName: c.name,\n            authorEmail: c.email,\n            authorIP: c.ip\n          }))\n        } else {\n          throw new WIKI.Error.CommentViewForbidden()\n        }\n      } else {\n        return []\n      }\n    },\n    /**\n     * Fetch a single comment\n     */\n    async single (obj, args, context) {\n      const cm = await WIKI.data.commentProvider.getCommentById(args.id)\n      if (!cm || !cm.pageId) {\n        throw new WIKI.Error.CommentNotFound()\n      }\n      const page = await WIKI.models.pages.query().select('localeCode', 'path').findById(cm.pageId)\n        .withGraphJoined('tags')\n        .modifyGraph('tags', builder => {\n          builder.select('tag')\n        })\n      if (page) {\n        if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], {\n          path: page.path,\n          locale: page.localeCode,\n          tags: page.tags\n        })) {\n          return {\n            ...cm,\n            authorName: cm.name,\n            authorEmail: cm.email,\n            authorIP: cm.ip\n          }\n        } else {\n          throw new WIKI.Error.CommentViewForbidden()\n        }\n      } else {\n        WIKI.logger.warn(`Comment #${cm.id} is linked to a page #${cm.pageId} that doesn't exist! [ERROR]`)\n        throw new WIKI.Error.CommentGenericError()\n      }\n    }\n  },\n  CommentMutation: {\n    /**\n     * Create New Comment\n     */\n    async create (obj, args, context) {\n      try {\n        const cmId = await WIKI.models.comments.postNewComment({\n          ...args,\n          user: context.req.user,\n          ip: context.req.ip\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('New comment posted successfully'),\n          id: cmId\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Update an Existing Comment\n     */\n    async update (obj, args, context) {\n      try {\n        const cmRender = await WIKI.models.comments.updateComment({\n          ...args,\n          user: context.req.user,\n          ip: context.req.ip\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Comment updated successfully'),\n          render: cmRender\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Delete an Existing Comment\n     */\n    async delete (obj, args, context) {\n      try {\n        await WIKI.models.comments.deleteComment({\n          id: args.id,\n          user: context.req.user,\n          ip: context.req.ip\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Comment deleted successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Update Comments Providers\n     */\n    async updateProviders(obj, args, context) {\n      try {\n        for (let provider of args.providers) {\n          await WIKI.models.commentProviders.query().patch({\n            isEnabled: provider.isEnabled,\n            config: _.reduce(provider.config, (result, value, key) => {\n              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))\n              return result\n            }, {})\n          }).where('key', provider.key)\n        }\n        await WIKI.models.commentProviders.initProvider()\n        return {\n          responseResult: graphHelper.generateSuccess('Comment Providers updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/contribute.js",
    "content": "const request = require('request-promise')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async contribute() { return {} }\n  },\n  ContributeQuery: {\n    async contributors(obj, args, context, info) {\n      try {\n        const resp = await request({\n          method: 'POST',\n          uri: 'https://graph.requarks.io',\n          json: true,\n          body: {\n            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',\n            variables: {}\n          }\n        })\n        return _.get(resp, 'data.sponsors.list', [])\n      } catch (err) {\n        WIKI.logger.warn(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/folder.js",
    "content": "module.exports = {\n  // Query: {\n  //   folders(obj, args, context, info) {\n  //     return WIKI.models.Folder.findAll({ where: args })\n  //   }\n  // },\n  // Mutation: {\n  //   createFolder(obj, args) {\n  //     return WIKI.models.Folder.create(args)\n  //   },\n  //   deleteFolder(obj, args) {\n  //     return WIKI.models.Folder.destroy({\n  //       where: {\n  //         id: args.id\n  //       },\n  //       limit: 1\n  //     })\n  //   },\n  //   renameFolder(obj, args) {\n  //     return WIKI.models.Folder.update({\n  //       name: args.name\n  //     }, {\n  //       where: { id: args.id }\n  //     })\n  //   }\n  // },\n  // Folder: {\n  //   files(grp) {\n  //     return grp.getFiles()\n  //   }\n  // }\n}\n"
  },
  {
    "path": "server/graph/resolvers/group.js",
    "content": "const graphHelper = require('../../helpers/graph')\nconst safeRegex = require('safe-regex')\nconst _ = require('lodash')\nconst gql = require('graphql')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async groups () { return {} }\n  },\n  Mutation: {\n    async groups () { return {} }\n  },\n  GroupQuery: {\n    /**\n     * FETCH ALL GROUPS\n     */\n    async list () {\n      return WIKI.models.groups.query().select(\n        'groups.*',\n        WIKI.models.groups.relatedQuery('users').count().as('userCount')\n      )\n    },\n    /**\n     * FETCH A SINGLE GROUP\n     */\n    async single(obj, args) {\n      return WIKI.models.groups.query().findById(args.id)\n    }\n  },\n  GroupMutation: {\n    /**\n     * ASSIGN USER TO GROUP\n     */\n    async assignUser (obj, args, { req }) {\n      // Check for guest user\n      if (args.userId === 2) {\n        throw new gql.GraphQLError('Cannot assign the Guest user to a group.')\n      }\n\n      // Check for valid group\n      const grp = await WIKI.models.groups.query().findById(args.groupId)\n      if (!grp) {\n        throw new gql.GraphQLError('Invalid Group ID')\n      }\n\n      // Check assigned permissions for write:groups\n      if (\n        WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) &&\n        grp.permissions.some(p => {\n          const resType = _.last(p.split(':'))\n          return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)\n        })\n      ) {\n        throw new gql.GraphQLError('You are not authorized to assign a user to this elevated group.')\n      }\n\n      // Check for valid user\n      const usr = await WIKI.models.users.query().findById(args.userId)\n      if (!usr) {\n        throw new gql.GraphQLError('Invalid User ID')\n      }\n\n      // Check for existing relation\n      const relExist = await WIKI.models.knex('userGroups').where({\n        userId: args.userId,\n        groupId: args.groupId\n      }).first()\n      if (relExist) {\n        throw new gql.GraphQLError('User is already assigned to group.')\n      }\n\n      // Assign user to group\n      await grp.$relatedQuery('users').relate(usr.id)\n\n      // Revoke tokens for this user\n      WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' })\n      WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })\n\n      return {\n        responseResult: graphHelper.generateSuccess('User has been assigned to group.')\n      }\n    },\n    /**\n     * CREATE NEW GROUP\n     */\n    async create (obj, args, { req }) {\n      const group = await WIKI.models.groups.query().insertAndFetch({\n        name: args.name,\n        permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),\n        pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules),\n        isSystem: false\n      })\n      await WIKI.auth.reloadGroups()\n      WIKI.events.outbound.emit('reloadGroups')\n      return {\n        responseResult: graphHelper.generateSuccess('Group created successfully.'),\n        group\n      }\n    },\n    /**\n     * DELETE GROUP\n     */\n    async delete (obj, args) {\n      if (args.id === 1 || args.id === 2) {\n        throw new gql.GraphQLError('Cannot delete this group.')\n      }\n\n      await WIKI.models.groups.query().deleteById(args.id)\n\n      WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' })\n      WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' })\n\n      await WIKI.auth.reloadGroups()\n      WIKI.events.outbound.emit('reloadGroups')\n\n      return {\n        responseResult: graphHelper.generateSuccess('Group has been deleted.')\n      }\n    },\n    /**\n     * UNASSIGN USER FROM GROUP\n     */\n    async unassignUser (obj, args) {\n      if (args.userId === 2) {\n        throw new gql.GraphQLError('Cannot unassign Guest user')\n      }\n      if (args.userId === 1 && args.groupId === 1) {\n        throw new gql.GraphQLError('Cannot unassign Administrator user from Administrators group.')\n      }\n      const grp = await WIKI.models.groups.query().findById(args.groupId)\n      if (!grp) {\n        throw new gql.GraphQLError('Invalid Group ID')\n      }\n      const usr = await WIKI.models.users.query().findById(args.userId)\n      if (!usr) {\n        throw new gql.GraphQLError('Invalid User ID')\n      }\n      await grp.$relatedQuery('users').unrelate().where('userId', usr.id)\n\n      WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' })\n      WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })\n\n      return {\n        responseResult: graphHelper.generateSuccess('User has been unassigned from group.')\n      }\n    },\n    /**\n     * UPDATE GROUP\n     */\n    async update (obj, args, { req }) {\n      // Check for unsafe regex page rules\n      if (_.some(args.pageRules, pr => {\n        return pr.match === 'REGEX' && !safeRegex(pr.path)\n      })) {\n        throw new gql.GraphQLError('Some Page Rules contains unsafe or exponential time regex.')\n      }\n\n      // Set default redirect on login value\n      if (_.isEmpty(args.redirectOnLogin)) {\n        args.redirectOnLogin = '/'\n      }\n\n      // Check assigned permissions for write:groups\n      if (\n        WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) &&\n        args.permissions.some(p => {\n          const resType = _.last(p.split(':'))\n          return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)\n        })\n      ) {\n        throw new gql.GraphQLError('You are not authorized to manage this group or assign these permissions.')\n      }\n\n      // Check assigned permissions for manage:groups\n      if (\n        WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) &&\n        args.permissions.some(p => _.last(p.split(':')) === 'system')\n      ) {\n        throw new gql.GraphQLError('You are not authorized to manage this group or assign the manage:system permissions.')\n      }\n\n      // Update group\n      await WIKI.models.groups.query().patch({\n        name: args.name,\n        redirectOnLogin: args.redirectOnLogin,\n        permissions: JSON.stringify(args.permissions),\n        pageRules: JSON.stringify(args.pageRules)\n      }).where('id', args.id)\n\n      // Revoke tokens for this group\n      WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' })\n      WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' })\n\n      // Reload group permissions\n      await WIKI.auth.reloadGroups()\n      WIKI.events.outbound.emit('reloadGroups')\n\n      return {\n        responseResult: graphHelper.generateSuccess('Group has been updated.')\n      }\n    }\n  },\n  Group: {\n    users (grp) {\n      return grp.$relatedQuery('users')\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/localization.js",
    "content": "const graphHelper = require('../../helpers/graph')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async localization() { return {} }\n  },\n  Mutation: {\n    async localization() { return {} }\n  },\n  LocalizationQuery: {\n    async locales(obj, args, context, info) {\n      let remoteLocales = await WIKI.cache.get('locales')\n      let localLocales = await WIKI.models.locales.query().select('code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt', 'availability')\n      remoteLocales = remoteLocales || localLocales\n      return _.map(remoteLocales, rl => {\n        let isInstalled = _.some(localLocales, ['code', rl.code])\n        return {\n          ...rl,\n          isInstalled,\n          installDate: isInstalled ? _.find(localLocales, ['code', rl.code]).updatedAt : null\n        }\n      })\n    },\n    async config(obj, args, context, info) {\n      return {\n        locale: WIKI.config.lang.code,\n        autoUpdate: WIKI.config.lang.autoUpdate,\n        namespacing: WIKI.config.lang.namespacing,\n        namespaces: WIKI.config.lang.namespaces\n      }\n    },\n    translations (obj, args, context, info) {\n      return WIKI.lang.getByNamespace(args.locale, args.namespace)\n    }\n  },\n  LocalizationMutation: {\n    async downloadLocale(obj, args, context) {\n      try {\n        const job = await WIKI.scheduler.registerJob({\n          name: 'fetch-graph-locale',\n          immediate: true\n        }, args.locale)\n        await job.finished\n        return {\n          responseResult: graphHelper.generateSuccess('Locale downloaded successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async updateLocale(obj, args, context) {\n      try {\n        WIKI.config.lang.code = args.locale\n        WIKI.config.lang.autoUpdate = args.autoUpdate\n        WIKI.config.lang.namespacing = args.namespacing\n        WIKI.config.lang.namespaces = _.union(args.namespaces, [args.locale])\n\n        const newLocale = await WIKI.models.locales.query().select('isRTL').where('code', args.locale).first()\n        WIKI.config.lang.rtl = newLocale.isRTL\n\n        await WIKI.configSvc.saveToDb(['lang'])\n\n        await WIKI.lang.setCurrentLocale(args.locale)\n        await WIKI.lang.refreshNamespaces()\n\n        await WIKI.cache.del('nav:locales')\n\n        return {\n          responseResult: graphHelper.generateSuccess('Locale config updated')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/logging.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async logging() { return {} }\n  },\n  Mutation: {\n    async logging() { return {} }\n  },\n  Subscription: {\n    loggingLiveTrail: {\n      subscribe: () => WIKI.GQLEmitter.asyncIterator('livetrail')\n    }\n  },\n  LoggingQuery: {\n    async loggers(obj, args, context, info) {\n      let loggers = await WIKI.models.loggers.getLoggers()\n      loggers = loggers.map(logger => {\n        const loggerInfo = _.find(WIKI.data.loggers, ['key', logger.key]) || {}\n        return {\n          ...loggerInfo,\n          ...logger,\n          config: _.sortBy(_.transform(logger.config, (res, value, key) => {\n            const configData = _.get(loggerInfo.props, key, {})\n            res.push({\n              key,\n              value: JSON.stringify({\n                ...configData,\n                value\n              })\n            })\n          }, []), 'key')\n        }\n      })\n      // if (args.filter) { loggers = graphHelper.filter(loggers, args.filter) }\n      if (args.orderBy) { loggers = _.sortBy(loggers, [args.orderBy]) }\n      return loggers\n    }\n  },\n  LoggingMutation: {\n    async updateLoggers(obj, args, context) {\n      try {\n        for (let logger of args.loggers) {\n          await WIKI.models.loggers.query().patch({\n            isEnabled: logger.isEnabled,\n            level: logger.level,\n            config: _.reduce(logger.config, (result, value, key) => {\n              _.set(result, `${value.key}`, value.value)\n              return result\n            }, {})\n          }).where('key', logger.key)\n        }\n        return {\n          responseResult: graphHelper.generateSuccess('Loggers updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/mail.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async mail() { return {} }\n  },\n  Mutation: {\n    async mail() { return {} }\n  },\n  MailQuery: {\n    async config(obj, args, context, info) {\n      return {\n        ...WIKI.config.mail,\n        pass: WIKI.config.mail.pass.length > 0 ? '********' : ''\n      }\n    }\n  },\n  MailMutation: {\n    async sendTest(obj, args, context) {\n      try {\n        if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {\n          throw new WIKI.Error.MailInvalidRecipient()\n        }\n\n        await WIKI.mail.send({\n          template: 'test',\n          to: args.recipientEmail,\n          subject: 'A test email from your wiki',\n          text: 'This is a test email sent from your wiki.',\n          data: {\n            preheadertext: 'This is a test email sent from your wiki.'\n          }\n        })\n\n        return {\n          responseResult: graphHelper.generateSuccess('Test email sent successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async updateConfig(obj, args, context) {\n      try {\n        WIKI.config.mail = {\n          senderName: args.senderName,\n          senderEmail: args.senderEmail,\n          host: args.host,\n          port: args.port,\n          name: args.name,\n          secure: args.secure,\n          verifySSL: args.verifySSL,\n          user: args.user,\n          pass: (args.pass === '********') ? WIKI.config.mail.pass : args.pass,\n          useDKIM: args.useDKIM,\n          dkimDomainName: args.dkimDomainName,\n          dkimKeySelector: args.dkimKeySelector,\n          dkimPrivateKey: args.dkimPrivateKey\n        }\n        await WIKI.configSvc.saveToDb(['mail'])\n\n        WIKI.mail.init()\n\n        return {\n          responseResult: graphHelper.generateSuccess('Mail configuration updated successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/navigation.js",
    "content": "const graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async navigation () { return {} }\n  },\n  Mutation: {\n    async navigation () { return {} }\n  },\n  NavigationQuery: {\n    async tree (obj, args, context, info) {\n      return WIKI.models.navigation.getTree({ cache: false, locale: 'all', bypassAuth: true })\n    },\n    config (obj, args, context, info) {\n      return WIKI.config.nav\n    }\n  },\n  NavigationMutation: {\n    async updateTree (obj, args, context) {\n      try {\n        await WIKI.models.navigation.query().patch({\n          config: args.tree\n        }).where('key', 'site')\n        for (const tree of args.tree) {\n          await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)\n        }\n\n        return {\n          responseResult: graphHelper.generateSuccess('Navigation updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async updateConfig (obj, args, context) {\n      try {\n        WIKI.config.nav = {\n          mode: args.mode\n        }\n        await WIKI.configSvc.saveToDb(['nav'])\n\n        return {\n          responseResult: graphHelper.generateSuccess('Navigation config updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/page.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async pages() { return {} }\n  },\n  Mutation: {\n    async pages() { return {} }\n  },\n  PageQuery: {\n    /**\n     * PAGE HISTORY\n     */\n    async history(obj, args, context, info) {\n      const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.id)\n      if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {\n        path: page.path,\n        locale: page.localeCode\n      })) {\n        return WIKI.models.pageHistory.getHistory({\n          pageId: args.id,\n          offsetPage: args.offsetPage || 0,\n          offsetSize: args.offsetSize || 100\n        })\n      } else {\n        throw new WIKI.Error.PageHistoryForbidden()\n      }\n    },\n    /**\n     * PAGE VERSION\n     */\n    async version(obj, args, context, info) {\n      const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId)\n      if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {\n        path: page.path,\n        locale: page.localeCode\n      })) {\n        return WIKI.models.pageHistory.getVersion({\n          pageId: args.pageId,\n          versionId: args.versionId\n        })\n      } else {\n        throw new WIKI.Error.PageHistoryForbidden()\n      }\n    },\n    /**\n     * SEARCH PAGES\n     */\n    async search (obj, args, context) {\n      if (WIKI.data.searchEngine) {\n        const resp = await WIKI.data.searchEngine.query(args.query, args)\n        return {\n          ...resp,\n          results: _.filter(resp.results, r => {\n            return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {\n              path: r.path,\n              locale: r.locale,\n              tags: r.tags // Tags are needed since access permissions can be limited by page tags too\n            })\n          })\n        }\n      } else {\n        return {\n          results: [],\n          suggestions: [],\n          totalHits: 0\n        }\n      }\n    },\n    /**\n     * LIST PAGES\n     */\n    async list (obj, args, context, info) {\n      let results = await WIKI.models.pages.query().column([\n        'pages.id',\n        'path',\n        { locale: 'localeCode' },\n        'title',\n        'description',\n        'isPublished',\n        'isPrivate',\n        'privateNS',\n        'contentType',\n        'createdAt',\n        'updatedAt'\n      ])\n        .withGraphJoined('tags')\n        .modifyGraph('tags', builder => {\n          builder.select('tag')\n        })\n        .modify(queryBuilder => {\n          if (args.limit) {\n            queryBuilder.limit(args.limit)\n          }\n          if (args.locale) {\n            queryBuilder.where('localeCode', args.locale)\n          }\n          if (args.creatorId && args.authorId && args.creatorId > 0 && args.authorId > 0) {\n            queryBuilder.where(function () {\n              this.where('creatorId', args.creatorId).orWhere('authorId', args.authorId)\n            })\n          } else {\n            if (args.creatorId && args.creatorId > 0) {\n              queryBuilder.where('creatorId', args.creatorId)\n            }\n            if (args.authorId && args.authorId > 0) {\n              queryBuilder.where('authorId', args.authorId)\n            }\n          }\n          if (args.tags && args.tags.length > 0) {\n            queryBuilder.whereIn('tags.tag', args.tags.map(t => _.trim(t).toLowerCase()))\n          }\n          const orderDir = args.orderByDirection === 'DESC' ? 'desc' : 'asc'\n          switch (args.orderBy) {\n            case 'CREATED':\n              queryBuilder.orderBy('createdAt', orderDir)\n              break\n            case 'PATH':\n              queryBuilder.orderBy('path', orderDir)\n              break\n            case 'TITLE':\n              queryBuilder.orderBy('title', orderDir)\n              break\n            case 'UPDATED':\n              queryBuilder.orderBy('updatedAt', orderDir)\n              break\n            default:\n              queryBuilder.orderBy('pages.id', orderDir)\n              break\n          }\n        })\n      results = _.filter(results, r => {\n        return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {\n          path: r.path,\n          locale: r.locale\n        })\n      }).map(r => ({\n        ...r,\n        tags: _.map(r.tags, 'tag')\n      }))\n      if (args.tags && args.tags.length > 0) {\n        results = _.filter(results, r => _.every(args.tags, t => _.includes(r.tags, t)))\n      }\n      return results\n    },\n    /**\n     * FETCH SINGLE PAGE\n     */\n    async single (obj, args, context, info) {\n      let page = await WIKI.models.pages.getPageFromDb(args.id)\n      if (page) {\n        if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {\n          path: page.path,\n          locale: page.localeCode\n        })) {\n          return {\n            ...page,\n            locale: page.localeCode,\n            editor: page.editorKey,\n            scriptJs: page.extra.js,\n            scriptCss: page.extra.css\n          }\n        } else {\n          throw new WIKI.Error.PageViewForbidden()\n        }\n      } else {\n        throw new WIKI.Error.PageNotFound()\n      }\n    },\n    async singleByPath(obj, args, context, info) {\n      let page = await WIKI.models.pages.getPageFromDb({\n        path: args.path,\n        locale: args.locale,\n      });\n      if (page) {\n        if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {\n          path: page.path,\n          locale: page.localeCode\n        })) {\n          return {\n            ...page,\n            locale: page.localeCode,\n            editor: page.editorKey,\n            scriptJs: page.extra.js,\n            scriptCss: page.extra.css\n          }\n        } else {\n          throw new WIKI.Error.PageViewForbidden()\n        }\n      } else {\n        throw new WIKI.Error.PageNotFound()\n      }\n    },\n    /**\n     * FETCH TAGS\n     */\n    async tags (obj, args, context, info) {\n      const pages = await WIKI.models.pages.query()\n        .column([\n          'path',\n          { locale: 'localeCode' }\n        ])\n        .withGraphJoined('tags')\n      const allTags = _.filter(pages, r => {\n        return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {\n          path: r.path,\n          locale: r.locale\n        })\n      }).flatMap(r => r.tags)\n      return _.orderBy(_.uniqBy(allTags, 'id'), ['tag'], ['asc'])\n    },\n    /**\n     * SEARCH TAGS\n     */\n    async searchTags (obj, args, context, info) {\n      const query = _.trim(args.query)\n      const pages = await WIKI.models.pages.query()\n        .column([\n          'path',\n          { locale: 'localeCode' }\n        ])\n        .withGraphJoined('tags')\n        .modifyGraph('tags', builder => {\n          builder.select('tag')\n        })\n        .modify(queryBuilder => {\n          queryBuilder.andWhere(builderSub => {\n            if (WIKI.config.db.type === 'postgres') {\n              builderSub.where('tags.tag', 'ILIKE', `%${query}%`)\n            } else {\n              builderSub.where('tags.tag', 'LIKE', `%${query}%`)\n            }\n          })\n        })\n      const allTags = _.filter(pages, r => {\n        return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {\n          path: r.path,\n          locale: r.locale\n        })\n      }).flatMap(r => r.tags).map(t => t.tag)\n      return _.uniq(allTags).slice(0, 5)\n    },\n    /**\n     * FETCH PAGE TREE\n     */\n    async tree (obj, args, context, info) {\n      let curPage = null\n\n      if (!args.locale) { args.locale = WIKI.config.lang.code }\n\n      if (args.path && !args.parent) {\n        curPage = await WIKI.models.knex('pageTree').first('parent', 'ancestors').where({\n          path: args.path,\n          localeCode: args.locale\n        })\n        if (curPage) {\n          args.parent = curPage.parent || 0\n        } else {\n          return []\n        }\n      }\n\n      const results = await WIKI.models.knex('pageTree').where(builder => {\n        builder.where('localeCode', args.locale)\n        switch (args.mode) {\n          case 'FOLDERS':\n            builder.andWhere('isFolder', true)\n            break\n          case 'PAGES':\n            builder.andWhereNotNull('pageId')\n            break\n        }\n        if (!args.parent || args.parent < 1) {\n          builder.whereNull('parent')\n        } else {\n          builder.where('parent', args.parent)\n          if (args.includeAncestors && curPage && curPage.ancestors.length > 0) {\n            builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors)\n          }\n        }\n      }).orderBy([{ column: 'isFolder', order: 'desc' }, 'title'])\n      return results.filter(r => {\n        return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {\n          path: r.path,\n          locale: r.localeCode\n        })\n      }).map(r => ({\n        ...r,\n        parent: r.parent || 0,\n        locale: r.localeCode\n      }))\n    },\n    /**\n     * FETCH PAGE LINKS\n     */\n    async links (obj, args, context, info) {\n      let results\n\n      if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') {\n        results = await WIKI.models.knex('pages')\n          .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' })\n          .leftJoin('pageLinks', 'pages.id', 'pageLinks.pageId')\n          .where({\n            'pages.localeCode': args.locale\n          })\n          .unionAll(\n            WIKI.models.knex('pageLinks')\n              .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' })\n              .leftJoin('pages', 'pageLinks.pageId', 'pages.id')\n              .where({\n                'pages.localeCode': args.locale\n              })\n          )\n      } else {\n        results = await WIKI.models.knex('pages')\n          .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' })\n          .fullOuterJoin('pageLinks', 'pages.id', 'pageLinks.pageId')\n          .where({\n            'pages.localeCode': args.locale\n          })\n      }\n\n      return _.reduce(results, (result, val) => {\n        // -> Check if user has access to source and linked page\n        if (\n          !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.path, locale: args.locale }) ||\n          !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.link, locale: val.locale })\n        ) {\n          return result\n        }\n\n        const existingEntry = _.findIndex(result, ['id', val.id])\n        if (existingEntry >= 0) {\n          if (val.link) {\n            result[existingEntry].links.push(`${val.locale}/${val.link}`)\n          }\n        } else {\n          result.push({\n            id: val.id,\n            title: val.title,\n            path: `${args.locale}/${val.path}`,\n            links: val.link ? [`${val.locale}/${val.link}`] : []\n          })\n        }\n        return result\n      }, [])\n    },\n    /**\n     * CHECK FOR EDITING CONFLICT\n     */\n    async checkConflicts (obj, args, context, info) {\n      let page = await WIKI.models.pages.query().select('path', 'localeCode', 'updatedAt').findById(args.id)\n      if (page) {\n        if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {\n          path: page.path,\n          locale: page.localeCode\n        })) {\n          return page.updatedAt > args.checkoutDate\n        } else {\n          throw new WIKI.Error.PageUpdateForbidden()\n        }\n      } else {\n        throw new WIKI.Error.PageNotFound()\n      }\n    },\n    /**\n     * FETCH LATEST VERSION FOR CONFLICT COMPARISON\n     */\n    async conflictLatest (obj, args, context, info) {\n      let page = await WIKI.models.pages.getPageFromDb(args.id)\n      if (page) {\n        if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {\n          path: page.path,\n          locale: page.localeCode\n        })) {\n          return {\n            ...page,\n            tags: page.tags.map(t => t.tag),\n            locale: page.localeCode\n          }\n        } else {\n          throw new WIKI.Error.PageViewForbidden()\n        }\n      } else {\n        throw new WIKI.Error.PageNotFound()\n      }\n    }\n  },\n  PageMutation: {\n    /**\n     * CREATE PAGE\n     */\n    async create(obj, args, context) {\n      try {\n        const page = await WIKI.models.pages.createPage({\n          ...args,\n          user: context.req.user\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Page created successfully.'),\n          page\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * UPDATE PAGE\n     */\n    async update(obj, args, context) {\n      try {\n        const page = await WIKI.models.pages.updatePage({\n          ...args,\n          user: context.req.user\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Page has been updated.'),\n          page\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * CONVERT PAGE\n     */\n    async convert(obj, args, context) {\n      try {\n        await WIKI.models.pages.convertPage({\n          ...args,\n          user: context.req.user\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Page has been converted.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * MOVE PAGE\n     */\n    async move(obj, args, context) {\n      try {\n        await WIKI.models.pages.movePage({\n          ...args,\n          user: context.req.user\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Page has been moved.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * DELETE PAGE\n     */\n    async delete(obj, args, context) {\n      try {\n        await WIKI.models.pages.deletePage({\n          ...args,\n          user: context.req.user\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Page has been deleted.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * DELETE TAG\n     */\n    async deleteTag (obj, args, context) {\n      try {\n        const tagToDel = await WIKI.models.tags.query().findById(args.id)\n        if (tagToDel) {\n          await tagToDel.$relatedQuery('pages').unrelate()\n          await WIKI.models.tags.query().deleteById(args.id)\n        } else {\n          throw new Error('This tag does not exist.')\n        }\n        return {\n          responseResult: graphHelper.generateSuccess('Tag has been deleted.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * UPDATE TAG\n     */\n    async updateTag (obj, args, context) {\n      try {\n        const affectedRows = await WIKI.models.tags.query()\n          .findById(args.id)\n          .patch({\n            tag: _.trim(args.tag).toLowerCase(),\n            title: _.trim(args.title)\n          })\n        if (affectedRows < 1) {\n          throw new Error('This tag does not exist.')\n        }\n        return {\n          responseResult: graphHelper.generateSuccess('Tag has been updated successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * FLUSH PAGE CACHE\n     */\n    async flushCache(obj, args, context) {\n      try {\n        await WIKI.models.pages.flushCache()\n        WIKI.events.outbound.emit('flushCache')\n        return {\n          responseResult: graphHelper.generateSuccess('Pages Cache has been flushed successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * MIGRATE ALL PAGES FROM SOURCE LOCALE TO TARGET LOCALE\n     */\n    async migrateToLocale(obj, args, context) {\n      try {\n        const count = await WIKI.models.pages.migrateToLocale(args)\n        return {\n          responseResult: graphHelper.generateSuccess('Migrated content to target locale successfully.'),\n          count\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * REBUILD TREE\n     */\n    async rebuildTree(obj, args, context) {\n      try {\n        await WIKI.models.pages.rebuildTree()\n        return {\n          responseResult: graphHelper.generateSuccess('Page tree rebuilt successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * RENDER PAGE\n     */\n    async render (obj, args, context) {\n      try {\n        const page = await WIKI.models.pages.query().findById(args.id)\n        if (!page) {\n          throw new WIKI.Error.PageNotFound()\n        }\n        await WIKI.models.pages.renderPage(page)\n        return {\n          responseResult: graphHelper.generateSuccess('Page rendered successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * RESTORE PAGE VERSION\n     */\n    async restore (obj, args, context) {\n      try {\n        const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId)\n        if (!page) {\n          throw new WIKI.Error.PageNotFound()\n        }\n\n        if (!WIKI.auth.checkAccess(context.req.user, ['write:pages'], {\n          path: page.path,\n          locale: page.localeCode\n        })) {\n          throw new WIKI.Error.PageRestoreForbidden()\n        }\n\n        const targetVersion = await WIKI.models.pageHistory.getVersion({ pageId: args.pageId, versionId: args.versionId })\n        if (!targetVersion) {\n          throw new WIKI.Error.PageNotFound()\n        }\n\n        await WIKI.models.pages.updatePage({\n          ...targetVersion,\n          id: targetVersion.pageId,\n          user: context.req.user,\n          action: 'restored'\n        })\n\n        return {\n          responseResult: graphHelper.generateSuccess('Page version restored successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Purge history\n     */\n    async purgeHistory (obj, args, context) {\n      try {\n        await WIKI.models.pageHistory.purge(args.olderThan)\n        return {\n          responseResult: graphHelper.generateSuccess('Page history purged successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  },\n  Page: {\n    async tags (obj) {\n      return WIKI.models.pages.relatedQuery('tags').for(obj.id)\n    }\n    // comments(pg) {\n    //   return pg.$relatedQuery('comments')\n    // }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/rendering.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async rendering() { return {} }\n  },\n  Mutation: {\n    async rendering() { return {} }\n  },\n  RenderingQuery: {\n    async renderers(obj, args, context, info) {\n      let renderers = await WIKI.models.renderers.getRenderers()\n      renderers = renderers.map(rdr => {\n        const rendererInfo = _.find(WIKI.data.renderers, ['key', rdr.key]) || {}\n        return {\n          ...rendererInfo,\n          ...rdr,\n          config: _.sortBy(_.transform(rdr.config, (res, value, key) => {\n            const configData = _.get(rendererInfo.props, key, false)\n            if (configData) {\n              res.push({\n                key,\n                value: JSON.stringify({\n                  ...configData,\n                  value\n                })\n              })\n            }\n          }, []), 'key')\n        }\n      })\n      // if (args.filter) { renderers = graphHelper.filter(renderers, args.filter) }\n      if (args.orderBy) { renderers = _.sortBy(renderers, [args.orderBy]) }\n      return renderers\n    }\n  },\n  RenderingMutation: {\n    async updateRenderers(obj, args, context) {\n      try {\n        for (let rdr of args.renderers) {\n          await WIKI.models.renderers.query().patch({\n            isEnabled: rdr.isEnabled,\n            config: _.reduce(rdr.config, (result, value, key) => {\n              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))\n              return result\n            }, {})\n          }).where('key', rdr.key)\n        }\n        return {\n          responseResult: graphHelper.generateSuccess('Renderers updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/search.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async search() { return {} }\n  },\n  Mutation: {\n    async search() { return {} }\n  },\n  SearchQuery: {\n    async searchEngines(obj, args, context, info) {\n      let searchEngines = await WIKI.models.searchEngines.getSearchEngines()\n      searchEngines = searchEngines.map(searchEngine => {\n        const searchEngineInfo = _.find(WIKI.data.searchEngines, ['key', searchEngine.key]) || {}\n        return {\n          ...searchEngineInfo,\n          ...searchEngine,\n          config: _.sortBy(_.transform(searchEngine.config, (res, value, key) => {\n            const configData = _.get(searchEngineInfo.props, key, false)\n            if (configData) {\n              res.push({\n                key,\n                value: JSON.stringify({\n                  ...configData,\n                  value\n                })\n              })\n            }\n          }, []), 'key')\n        }\n      })\n      // if (args.filter) { searchEngines = graphHelper.filter(searchEngines, args.filter) }\n      if (args.orderBy) { searchEngines = _.sortBy(searchEngines, [args.orderBy]) }\n      return searchEngines\n    }\n  },\n  SearchMutation: {\n    async updateSearchEngines(obj, args, context) {\n      try {\n        let newActiveEngine = ''\n        for (let searchEngine of args.engines) {\n          if (searchEngine.isEnabled) {\n            newActiveEngine = searchEngine.key\n          }\n          await WIKI.models.searchEngines.query().patch({\n            isEnabled: searchEngine.isEnabled,\n            config: _.reduce(searchEngine.config, (result, value, key) => {\n              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))\n              return result\n            }, {})\n          }).where('key', searchEngine.key)\n        }\n        if (newActiveEngine !== WIKI.data.searchEngine.key) {\n          try {\n            await WIKI.data.searchEngine.deactivate()\n          } catch (err) {\n            WIKI.logger.warn('Failed to deactivate previous search engine:', err)\n          }\n        }\n        await WIKI.models.searchEngines.initEngine({ activate: true })\n        return {\n          responseResult: graphHelper.generateSuccess('Search Engines updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async rebuildIndex (obj, args, context) {\n      try {\n        await WIKI.data.searchEngine.rebuild()\n        return {\n          responseResult: graphHelper.generateSuccess('Index rebuilt successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/site.js",
    "content": "const graphHelper = require('../../helpers/graph')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async site() { return {} }\n  },\n  Mutation: {\n    async site() { return {} }\n  },\n  SiteQuery: {\n    async config(obj, args, context, info) {\n      return {\n        host: WIKI.config.host,\n        title: WIKI.config.title,\n        company: WIKI.config.company,\n        contentLicense: WIKI.config.contentLicense,\n        footerOverride: WIKI.config.footerOverride,\n        logoUrl: WIKI.config.logoUrl,\n        pageExtensions: WIKI.config.pageExtensions.join(', '),\n        ...WIKI.config.seo,\n        ...WIKI.config.editShortcuts,\n        ...WIKI.config.features,\n        ...WIKI.config.security,\n        authAutoLogin: WIKI.config.auth.autoLogin,\n        authEnforce2FA: WIKI.config.auth.enforce2FA,\n        authHideLocal: WIKI.config.auth.hideLocal,\n        authLoginBgUrl: WIKI.config.auth.loginBgUrl,\n        authJwtAudience: WIKI.config.auth.audience,\n        authJwtExpiration: WIKI.config.auth.tokenExpiration,\n        authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal,\n        uploadMaxFileSize: WIKI.config.uploads.maxFileSize,\n        uploadMaxFiles: WIKI.config.uploads.maxFiles,\n        uploadScanSVG: WIKI.config.uploads.scanSVG,\n        uploadForceDownload: WIKI.config.uploads.forceDownload\n      }\n    }\n  },\n  SiteMutation: {\n    async updateConfig(obj, args, context) {\n      try {\n        if (args.hasOwnProperty('host')) {\n          let siteHost = _.trim(args.host)\n          if (siteHost.endsWith('/')) {\n            siteHost = siteHost.slice(0, -1)\n          }\n          WIKI.config.host = siteHost\n        }\n\n        if (args.hasOwnProperty('title')) {\n          WIKI.config.title = _.trim(args.title)\n        }\n\n        if (args.hasOwnProperty('company')) {\n          WIKI.config.company = _.trim(args.company)\n        }\n\n        if (args.hasOwnProperty('contentLicense')) {\n          WIKI.config.contentLicense = args.contentLicense\n        }\n\n        if (args.hasOwnProperty('footerOverride')) {\n          WIKI.config.footerOverride = args.footerOverride\n        }\n\n        if (args.hasOwnProperty('logoUrl')) {\n          WIKI.config.logoUrl = _.trim(args.logoUrl)\n        }\n\n        if (args.hasOwnProperty('pageExtensions')) {\n          WIKI.config.pageExtensions = _.trim(args.pageExtensions).split(',').map(p => p.trim().toLowerCase()).filter(p => p !== '')\n        }\n\n        WIKI.config.seo = {\n          description: _.get(args, 'description', WIKI.config.seo.description),\n          robots: _.get(args, 'robots', WIKI.config.seo.robots),\n          analyticsService: _.get(args, 'analyticsService', WIKI.config.seo.analyticsService),\n          analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId)\n        }\n\n        WIKI.config.auth = {\n          autoLogin: _.get(args, 'authAutoLogin', WIKI.config.auth.autoLogin),\n          enforce2FA: _.get(args, 'authEnforce2FA', WIKI.config.auth.enforce2FA),\n          hideLocal: _.get(args, 'authHideLocal', WIKI.config.auth.hideLocal),\n          loginBgUrl: _.get(args, 'authLoginBgUrl', WIKI.config.auth.loginBgUrl),\n          audience: _.get(args, 'authJwtAudience', WIKI.config.auth.audience),\n          tokenExpiration: _.get(args, 'authJwtExpiration', WIKI.config.auth.tokenExpiration),\n          tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal)\n        }\n\n        WIKI.config.editShortcuts = {\n          editFab: _.get(args, 'editFab', WIKI.config.editShortcuts.editFab),\n          editMenuBar: _.get(args, 'editMenuBar', WIKI.config.editShortcuts.editMenuBar),\n          editMenuBtn: _.get(args, 'editMenuBtn', WIKI.config.editShortcuts.editMenuBtn),\n          editMenuExternalBtn: _.get(args, 'editMenuExternalBtn', WIKI.config.editShortcuts.editMenuExternalBtn),\n          editMenuExternalName: _.get(args, 'editMenuExternalName', WIKI.config.editShortcuts.editMenuExternalName),\n          editMenuExternalIcon: _.get(args, 'editMenuExternalIcon', WIKI.config.editShortcuts.editMenuExternalIcon),\n          editMenuExternalUrl: _.get(args, 'editMenuExternalUrl', WIKI.config.editShortcuts.editMenuExternalUrl)\n        }\n\n        WIKI.config.features = {\n          featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings),\n          featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments),\n          featurePersonalWikis: _.get(args, 'featurePersonalWikis', WIKI.config.features.featurePersonalWikis)\n        }\n\n        WIKI.config.security = {\n          securityOpenRedirect: _.get(args, 'securityOpenRedirect', WIKI.config.security.securityOpenRedirect),\n          securityIframe: _.get(args, 'securityIframe', WIKI.config.security.securityIframe),\n          securityReferrerPolicy: _.get(args, 'securityReferrerPolicy', WIKI.config.security.securityReferrerPolicy),\n          securityTrustProxy: _.get(args, 'securityTrustProxy', WIKI.config.security.securityTrustProxy),\n          securitySRI: _.get(args, 'securitySRI', WIKI.config.security.securitySRI),\n          securityHSTS: _.get(args, 'securityHSTS', WIKI.config.security.securityHSTS),\n          securityHSTSDuration: _.get(args, 'securityHSTSDuration', WIKI.config.security.securityHSTSDuration),\n          securityCSP: _.get(args, 'securityCSP', WIKI.config.security.securityCSP),\n          securityCSPDirectives: _.get(args, 'securityCSPDirectives', WIKI.config.security.securityCSPDirectives)\n        }\n\n        WIKI.config.uploads = {\n          maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize),\n          maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles),\n          scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG),\n          forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)\n        }\n\n        await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'footerOverride', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'editShortcuts', 'features', 'security', 'uploads'])\n\n        if (WIKI.config.security.securityTrustProxy) {\n          WIKI.app.enable('trust proxy')\n        } else {\n          WIKI.app.disable('trust proxy')\n        }\n\n        return {\n          responseResult: graphHelper.generateSuccess('Site configuration updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/storage.js",
    "content": "const _ = require('lodash')\nconst graphHelper = require('../../helpers/graph')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async storage() { return {} }\n  },\n  Mutation: {\n    async storage() { return {} }\n  },\n  StorageQuery: {\n    async targets(obj, args, context, info) {\n      let targets = await WIKI.models.storage.getTargets()\n      targets = _.sortBy(targets.map(tgt => {\n        const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {}\n        return {\n          ...targetInfo,\n          ...tgt,\n          hasSchedule: (targetInfo.schedule !== false),\n          syncInterval: tgt.syncInterval || targetInfo.schedule || 'P0D',\n          syncIntervalDefault: targetInfo.schedule,\n          config: _.sortBy(_.transform(tgt.config, (res, value, key) => {\n            const configData = _.get(targetInfo.props, key, false)\n            if (configData) {\n              res.push({\n                key,\n                value: JSON.stringify({\n                  ...configData,\n                  value: (configData.sensitive && value.length > 0) ? '********' : value\n                })\n              })\n            }\n          }, []), 'key')\n        }\n      }), ['title', 'key'])\n      return targets\n    },\n    async status(obj, args, context, info) {\n      let activeTargets = await WIKI.models.storage.query().where('isEnabled', true)\n      return activeTargets.map(tgt => {\n        const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {}\n        return {\n          key: tgt.key,\n          title: targetInfo.title,\n          status: _.get(tgt, 'state.status', 'pending'),\n          message: _.get(tgt, 'state.message', 'Initializing...'),\n          lastAttempt: _.get(tgt, 'state.lastAttempt', null)\n        }\n      })\n    }\n  },\n  StorageMutation: {\n    async updateTargets(obj, args, context) {\n      try {\n        let dbTargets = await WIKI.models.storage.getTargets()\n        for (let tgt of args.targets) {\n          const currentDbTarget = _.find(dbTargets, ['key', tgt.key])\n          if (!currentDbTarget) {\n            continue\n          }\n          await WIKI.models.storage.query().patch({\n            isEnabled: tgt.isEnabled,\n            mode: tgt.mode,\n            syncInterval: tgt.syncInterval,\n            config: _.reduce(tgt.config, (result, value, key) => {\n              let configValue = _.get(JSON.parse(value.value), 'v', null)\n              if (configValue === '********') {\n                configValue = _.get(currentDbTarget.config, value.key, '')\n              }\n              _.set(result, `${value.key}`, configValue)\n              return result\n            }, {}),\n            state: {\n              status: 'pending',\n              message: 'Initializing...',\n              lastAttempt: null\n            }\n          }).where('key', tgt.key)\n        }\n        await WIKI.models.storage.initTargets()\n        return {\n          responseResult: graphHelper.generateSuccess('Storage targets updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async executeAction(obj, args, context) {\n      try {\n        await WIKI.models.storage.executeAction(args.targetKey, args.handler)\n        return {\n          responseResult: graphHelper.generateSuccess('Action completed.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/system.js",
    "content": "const _ = require('lodash')\nconst getos = require('getos')\nconst os = require('os')\nconst filesize = require('filesize')\nconst path = require('path')\nconst fs = require('fs-extra')\nconst moment = require('moment')\nconst graphHelper = require('../../helpers/graph')\nconst request = require('request-promise')\nconst crypto = require('crypto')\nconst nanoid = require('nanoid/non-secure').customAlphabet('1234567890abcdef', 10)\n\nconst getosAsync = require('util').promisify(getos)\n\n/* global WIKI */\n\nconst dbTypes = {\n  mysql: 'MySQL',\n  mariadb: 'MariaDB',\n  postgres: 'PostgreSQL',\n  sqlite: 'SQLite',\n  mssql: 'MS SQL Server'\n}\n\nmodule.exports = {\n  Query: {\n    async system () { return {} }\n  },\n  Mutation: {\n    async system () { return {} }\n  },\n  SystemQuery: {\n    flags () {\n      return _.transform(WIKI.config.flags, (result, value, key) => {\n        result.push({ key, value })\n      }, [])\n    },\n    async info () { return {} },\n    async extensions () {\n      const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled']))\n      for (let ext of exts) {\n        ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()\n      }\n      return exts\n    },\n    async exportStatus () {\n      return {\n        status: WIKI.system.exportStatus.status,\n        progress: Math.ceil(WIKI.system.exportStatus.progress),\n        message: WIKI.system.exportStatus.message,\n        startedAt: WIKI.system.exportStatus.startedAt\n      }\n    }\n  },\n  SystemMutation: {\n    async updateFlags (obj, args, context) {\n      WIKI.config.flags = _.transform(args.flags, (result, row) => {\n        _.set(result, row.key, row.value)\n      }, {})\n      await WIKI.configSvc.applyFlags()\n      await WIKI.configSvc.saveToDb(['flags'])\n      return {\n        responseResult: graphHelper.generateSuccess('System Flags applied successfully')\n      }\n    },\n    async resetTelemetryClientId (obj, args, context) {\n      try {\n        WIKI.telemetry.generateClientId()\n        await WIKI.configSvc.saveToDb(['telemetry'])\n        return {\n          responseResult: graphHelper.generateSuccess('Telemetry state updated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async setTelemetry (obj, args, context) {\n      try {\n        _.set(WIKI.config, 'telemetry.isEnabled', args.enabled)\n        WIKI.telemetry.enabled = args.enabled\n        await WIKI.configSvc.saveToDb(['telemetry'])\n        return {\n          responseResult: graphHelper.generateSuccess('Telemetry Client ID has been reset successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async performUpgrade (obj, args, context) {\n      try {\n        if (process.env.UPGRADE_COMPANION) {\n          await request({\n            method: 'POST',\n            uri: 'http://wiki-update-companion/upgrade',\n            qs: {\n              ...process.env.UPGRADE_COMPANION_REF && { container: process.env.UPGRADE_COMPANION_REF }\n            }\n          })\n          return {\n            responseResult: graphHelper.generateSuccess('Upgrade has started.')\n          }\n        } else {\n          throw new Error('You must run the wiki-update-companion container and pass the UPGRADE_COMPANION env var in order to use this feature.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Import Users from a v1 installation\n     */\n    async importUsersFromV1(obj, args, context) {\n      try {\n        const MongoClient = require('mongodb').MongoClient\n        if (args.mongoDbConnString && args.mongoDbConnString.length > 10) {\n          // -> Connect to DB\n\n          const client = await MongoClient.connect(args.mongoDbConnString, {\n            appname: `Wiki.js ${WIKI.version} Migration Tool`\n          })\n          const dbUsers = client.db().collection('users')\n          const userCursor = dbUsers.find({ email: { '$ne': 'guest' } })\n\n          const curDateISO = new Date().toISOString()\n\n          let failed = []\n          let usersCount = 0\n          let groupsCount = 0\n          let assignableGroups = []\n          let reuseGroups = []\n\n          // -> Create SINGLE group\n\n          if (args.groupMode === `SINGLE`) {\n            const singleGroup = await WIKI.models.groups.query().insert({\n              name: `Import_${curDateISO}`,\n              permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),\n              pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules)\n            })\n            groupsCount++\n            assignableGroups.push(singleGroup.id)\n          }\n\n          // -> Iterate all users\n\n          while (await userCursor.hasNext()) {\n            const usr = await userCursor.next()\n\n            let usrGroup = []\n            if (args.groupMode === `MULTI`) {\n              // -> Check if global admin\n\n              if (_.some(usr.rights, ['role', 'admin'])) {\n                usrGroup.push(1)\n              } else {\n                // -> Check if identical group already exists\n\n                const currentRights = _.sortBy(_.map(usr.rights, r => _.pick(r, ['role', 'path', 'exact', 'deny'])), ['role', 'path', 'exact', 'deny'])\n                const ruleSetId = crypto.createHash('sha1').update(JSON.stringify(currentRights)).digest('base64')\n                const existingGroup = _.find(reuseGroups, ['hash', ruleSetId])\n                if (existingGroup) {\n                  usrGroup.push(existingGroup.groupId)\n                } else {\n                  // -> Build new group\n\n                  const pageRules = _.map(usr.rights, r => {\n                    let roles = ['read:pages', 'read:assets', 'read:comments', 'write:comments']\n                    if (r.role === `write`) {\n                      roles = _.concat(roles, ['write:pages', 'manage:pages', 'read:source', 'read:history', 'write:assets', 'manage:assets'])\n                    }\n                    return {\n                      id: nanoid(),\n                      roles: roles,\n                      match: r.exact ? 'EXACT' : 'START',\n                      deny: r.deny,\n                      path: (r.path.indexOf('/') === 0) ? r.path.substring(1) : r.path,\n                      locales: []\n                    }\n                  })\n\n                  const perms = _.chain(pageRules).reject('deny').map('roles').union().flatten().value()\n\n                  // -> Create new group\n\n                  const newGroup = await WIKI.models.groups.query().insert({\n                    name: `Import_${curDateISO}_${groupsCount + 1}`,\n                    permissions: JSON.stringify(perms),\n                    pageRules: JSON.stringify(pageRules)\n                  })\n                  reuseGroups.push({\n                    groupId: newGroup.id,\n                    hash: ruleSetId\n                  })\n                  groupsCount++\n                  usrGroup.push(newGroup.id)\n                }\n              }\n            }\n\n            // -> Create User\n\n            try {\n              await WIKI.models.users.createNewUser({\n                providerKey: usr.provider,\n                email: usr.email,\n                name: usr.name,\n                passwordRaw: usr.password,\n                groups: (usrGroup.length > 0) ? usrGroup : assignableGroups,\n                mustChangePassword: false,\n                sendWelcomeEmail: false\n              })\n              usersCount++\n            } catch (err) {\n              failed.push({\n                provider: usr.provider,\n                email: usr.email,\n                error: err.message\n              })\n              WIKI.logger.warn(`${usr.email}: ${err}`)\n            }\n          }\n\n          // -> Reload group permissions\n\n          if (args.groupMode !== `NONE`) {\n            await WIKI.auth.reloadGroups()\n            WIKI.events.outbound.emit('reloadGroups')\n          }\n\n          client.close()\n          return {\n            responseResult: graphHelper.generateSuccess('Import completed.'),\n            usersCount: usersCount,\n            groupsCount: groupsCount,\n            failed: failed\n          }\n        } else {\n          throw new Error('MongoDB Connection String is missing or invalid.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    /**\n     * Set HTTPS Redirection State\n     */\n    async setHTTPSRedirection (obj, args, context) {\n      _.set(WIKI.config, 'server.sslRedir', args.enabled)\n      await WIKI.configSvc.saveToDb(['server'])\n      return {\n        responseResult: graphHelper.generateSuccess('HTTP Redirection state set successfully.')\n      }\n    },\n    /**\n     * Renew SSL Certificate\n     */\n    async renewHTTPSCertificate (obj, args, context) {\n      try {\n        if (!WIKI.config.ssl.enabled) {\n          throw new WIKI.Error.SystemSSLDisabled()\n        } else if (WIKI.config.ssl.provider !== `letsencrypt`) {\n          throw new WIKI.Error.SystemSSLRenewInvalidProvider()\n        } else if (!WIKI.servers.le) {\n          throw new WIKI.Error.SystemSSLLEUnavailable()\n        } else {\n          await WIKI.servers.le.requestCertificate()\n          await WIKI.servers.restartServer('https')\n          return {\n            responseResult: graphHelper.generateSuccess('SSL Certificate renewed successfully.')\n          }\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n\n    /**\n     * Export Wiki to Disk\n     */\n    async export (obj, args, context) {\n      try {\n        const desiredPath = path.resolve(WIKI.ROOTPATH, args.path)\n        // -> Check if export process is already running\n        if (WIKI.system.exportStatus.status === 'running') {\n          throw new Error('Another export is already running.')\n        }\n        // -> Validate entities\n        if (args.entities.length < 1) {\n          throw new Error('Must specify at least 1 entity to export.')\n        }\n        // -> Check target path\n        await fs.ensureDir(desiredPath)\n        const existingFiles = await fs.readdir(desiredPath)\n        if (existingFiles.length) {\n          throw new Error('Target directory must be empty!')\n        }\n        // -> Start export\n        WIKI.system.export({\n          entities: args.entities,\n          path: desiredPath\n        })\n        return {\n          responseResult: graphHelper.generateSuccess('Export started successfully.')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  },\n  SystemInfo: {\n    configFile () {\n      return path.join(process.cwd(), 'config.yml')\n    },\n    cpuCores () {\n      return os.cpus().length\n    },\n    currentVersion () {\n      return WIKI.version\n    },\n    dbType () {\n      return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB')\n    },\n    async dbVersion () {\n      let version = 'Unknown Version'\n      switch (WIKI.config.db.type) {\n        case 'mariadb':\n        case 'mysql':\n          const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;')\n          version = _.get(resultMYSQL, '[0][0].version', 'Unknown Version')\n          break\n        case 'mssql':\n          const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;')\n          version = _.get(resultMSSQL, '[0].version', 'Unknown Version')\n          break\n        case 'postgres':\n          version = _.get(WIKI.models, 'knex.client.version', 'Unknown Version')\n          break\n        case 'sqlite':\n          version = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown Version')\n          break\n      }\n      return version\n    },\n    dbHost () {\n      if (WIKI.config.db.type === 'sqlite') {\n        return WIKI.config.db.storage\n      } else {\n        return WIKI.config.db.host\n      }\n    },\n    hostname () {\n      return os.hostname()\n    },\n    httpPort () {\n      return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0\n    },\n    httpRedirection () {\n      return _.get(WIKI.config, 'server.sslRedir', false)\n    },\n    httpsPort () {\n      return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0\n    },\n    latestVersion () {\n      return WIKI.system.updates.version\n    },\n    latestVersionReleaseDate () {\n      return moment.utc(WIKI.system.updates.releaseDate)\n    },\n    nodeVersion () {\n      return process.version.substr(1)\n    },\n    async operatingSystem () {\n      let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`\n      if (os.platform() === 'linux') {\n        const osInfo = await getosAsync()\n        osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`\n      }\n      return osLabel\n    },\n    async platform () {\n      const isDockerized = await fs.pathExists('/.dockerenv')\n      if (isDockerized) {\n        return 'docker'\n      }\n      return os.platform()\n    },\n    ramTotal () {\n      return filesize(os.totalmem())\n    },\n    sslDomain () {\n      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.domain : null\n    },\n    sslExpirationDate () {\n      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null\n    },\n    sslProvider () {\n      return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null\n    },\n    sslStatus () {\n      return 'OK'\n    },\n    sslSubscriberEmail () {\n      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.subscriberEmail : null\n    },\n    telemetry () {\n      return WIKI.telemetry.enabled\n    },\n    telemetryClientId () {\n      return WIKI.config.telemetry.clientId\n    },\n    async upgradeCapable () {\n      return !_.isNil(process.env.UPGRADE_COMPANION)\n    },\n    workingDirectory () {\n      return process.cwd()\n    },\n    async groupsTotal () {\n      const total = await WIKI.models.groups.query().count('* as total').first()\n      return _.toSafeInteger(total.total)\n    },\n    async pagesTotal () {\n      const total = await WIKI.models.pages.query().count('* as total').first()\n      return _.toSafeInteger(total.total)\n    },\n    async usersTotal () {\n      const total = await WIKI.models.users.query().count('* as total').first()\n      return _.toSafeInteger(total.total)\n    },\n    async tagsTotal () {\n      const total = await WIKI.models.tags.query().count('* as total').first()\n      return _.toSafeInteger(total.total)\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/tag.js",
    "content": "module.exports = {\n  // Query: {\n  //   tags(obj, args, context, info) {\n  //     return WIKI.models.Tag.findAll({ where: args })\n  //   }\n  // },\n  // Mutation: {\n  //   assignTagToDocument(obj, args) {\n  //     return WIKI.models.Tag.findById(args.tagId).then(tag => {\n  //       if (!tag) {\n  //         throw new gql.GraphQLError('Invalid Tag ID')\n  //       }\n  //       return WIKI.models.Document.findById(args.documentId).then(doc => {\n  //         if (!doc) {\n  //           throw new gql.GraphQLError('Invalid Document ID')\n  //         }\n  //         return tag.addDocument(doc)\n  //       })\n  //     })\n  //   },\n  //   createTag(obj, args) {\n  //     return WIKI.models.Tag.create(args)\n  //   },\n  //   deleteTag(obj, args) {\n  //     return WIKI.models.Tag.destroy({\n  //       where: {\n  //         id: args.id\n  //       },\n  //       limit: 1\n  //     })\n  //   },\n  //   removeTagFromDocument(obj, args) {\n  //     return WIKI.models.Tag.findById(args.tagId).then(tag => {\n  //       if (!tag) {\n  //         throw new gql.GraphQLError('Invalid Tag ID')\n  //       }\n  //       return WIKI.models.Document.findById(args.documentId).then(doc => {\n  //         if (!doc) {\n  //           throw new gql.GraphQLError('Invalid Document ID')\n  //         }\n  //         return tag.removeDocument(doc)\n  //       })\n  //     })\n  //   },\n  //   renameTag(obj, args) {\n  //     return WIKI.models.Group.update({\n  //       key: args.key\n  //     }, {\n  //       where: { id: args.id }\n  //     })\n  //   }\n  // },\n  // Tag: {\n  //   documents(tag) {\n  //     return tag.getDocuments()\n  //   }\n  // }\n}\n"
  },
  {
    "path": "server/graph/resolvers/theming.js",
    "content": "const graphHelper = require('../../helpers/graph')\nconst _ = require('lodash')\nconst CleanCSS = require('clean-css')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async theming() { return {} }\n  },\n  Mutation: {\n    async theming() { return {} }\n  },\n  ThemingQuery: {\n    async themes(obj, args, context, info) {\n      return [{ // TODO\n        key: 'default',\n        title: 'Default',\n        author: 'requarks.io'\n      }]\n    },\n    async config(obj, args, context, info) {\n      return {\n        theme: WIKI.config.theming.theme,\n        iconset: WIKI.config.theming.iconset,\n        darkMode: WIKI.config.theming.darkMode,\n        tocPosition: WIKI.config.theming.tocPosition || 'left',\n        injectCSS: new CleanCSS({ format: 'beautify' }).minify(WIKI.config.theming.injectCSS).styles,\n        injectHead: WIKI.config.theming.injectHead,\n        injectBody: WIKI.config.theming.injectBody\n      }\n    }\n  },\n  ThemingMutation: {\n    async setConfig(obj, args, context, info) {\n      try {\n        if (!_.isEmpty(args.injectCSS)) {\n          args.injectCSS = new CleanCSS({\n            inline: false\n          }).minify(args.injectCSS).styles\n        }\n\n        WIKI.config.theming = {\n          ...WIKI.config.theming,\n          theme: args.theme,\n          iconset: args.iconset,\n          darkMode: args.darkMode,\n          tocPosition: args.tocPosition || 'left',\n          injectCSS: args.injectCSS || '',\n          injectHead: args.injectHead || '',\n          injectBody: args.injectBody || ''\n        }\n\n        await WIKI.configSvc.saveToDb(['theming'])\n\n        return {\n          responseResult: graphHelper.generateSuccess('Theme config updated')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/resolvers/user.js",
    "content": "const graphHelper = require('../../helpers/graph')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = {\n  Query: {\n    async users() { return {} }\n  },\n  Mutation: {\n    async users() { return {} }\n  },\n  UserQuery: {\n    async list(obj, args, context, info) {\n      return WIKI.models.users.query()\n        .select('id', 'email', 'name', 'providerKey', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')\n    },\n    async search(obj, args, context, info) {\n      return WIKI.models.users.query()\n        .where('email', 'like', `%${args.query}%`)\n        .orWhere('name', 'like', `%${args.query}%`)\n        .limit(10)\n        .select('id', 'email', 'name', 'providerKey', 'createdAt')\n    },\n    async single(obj, args, context, info) {\n      let usr = await WIKI.models.users.query().findById(args.id)\n      usr.password = ''\n      usr.tfaSecret = ''\n\n      const str = _.get(WIKI.auth.strategies, usr.providerKey)\n      str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])\n      usr.providerName = str.displayName\n      usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)\n\n      return usr\n    },\n    async profile (obj, args, context, info) {\n      if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {\n        throw new WIKI.Error.AuthRequired()\n      }\n      const usr = await WIKI.models.users.query().findById(context.req.user.id)\n      if (!usr.isActive) {\n        throw new WIKI.Error.AuthAccountBanned()\n      }\n\n      const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {})\n\n      usr.providerName = providerInfo.displayName || 'Unknown'\n      usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt\n      usr.password = ''\n      usr.providerId = ''\n      usr.tfaSecret = ''\n\n      return usr\n    },\n    async lastLogins (obj, args, context, info) {\n      return WIKI.models.users.query()\n        .select('id', 'name', 'lastLoginAt')\n        .whereNotNull('lastLoginAt')\n        .orderBy('lastLoginAt', 'desc')\n        .limit(10)\n    }\n  },\n  UserMutation: {\n    async create (obj, args) {\n      try {\n        await WIKI.models.users.createNewUser(args)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User created successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async delete (obj, args) {\n      try {\n        if (args.id <= 2) {\n          throw new WIKI.Error.UserDeleteProtected()\n        }\n        await WIKI.models.users.deleteUser(args.id, args.replaceId)\n\n        WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' })\n        WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })\n\n        return {\n          responseResult: graphHelper.generateSuccess('User deleted successfully')\n        }\n      } catch (err) {\n        if (err.message.indexOf('foreign') >= 0) {\n          return graphHelper.generateError(new WIKI.Error.UserDeleteForeignConstraint())\n        } else {\n          return graphHelper.generateError(err)\n        }\n      }\n    },\n    async update (obj, args) {\n      try {\n        await WIKI.models.users.updateUser(args)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User created successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async verify (obj, args) {\n      try {\n        await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User verified successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async activate (obj, args) {\n      try {\n        await WIKI.models.users.query().patch({ isActive: true }).findById(args.id)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User activated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async deactivate (obj, args) {\n      try {\n        if (args.id <= 2) {\n          throw new Error('Cannot deactivate system accounts.')\n        }\n        await WIKI.models.users.query().patch({ isActive: false }).findById(args.id)\n\n        WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' })\n        WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })\n\n        return {\n          responseResult: graphHelper.generateSuccess('User deactivated successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async enableTFA (obj, args) {\n      try {\n        await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User 2FA enabled successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async disableTFA (obj, args) {\n      try {\n        await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User 2FA disabled successfully')\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    resetPassword (obj, args) {\n      return false\n    },\n    async updateProfile (obj, args, context) {\n      try {\n        if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {\n          throw new WIKI.Error.AuthRequired()\n        }\n        const usr = await WIKI.models.users.query().findById(context.req.user.id)\n        if (!usr.isActive) {\n          throw new WIKI.Error.AuthAccountBanned()\n        }\n        if (!usr.isVerified) {\n          throw new WIKI.Error.AuthAccountNotVerified()\n        }\n\n        if (!['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {\n          throw new WIKI.Error.InputInvalid()\n        }\n\n        if (!['', 'light', 'dark'].includes(args.appearance)) {\n          throw new WIKI.Error.InputInvalid()\n        }\n\n        await WIKI.models.users.updateUser({\n          id: usr.id,\n          name: _.trim(args.name),\n          jobTitle: _.trim(args.jobTitle),\n          location: _.trim(args.location),\n          timezone: args.timezone,\n          dateFormat: args.dateFormat,\n          appearance: args.appearance\n        })\n\n        const newToken = await WIKI.models.users.refreshToken(usr.id)\n\n        return {\n          responseResult: graphHelper.generateSuccess('User profile updated successfully'),\n          jwt: newToken.token\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    },\n    async changePassword (obj, args, context) {\n      try {\n        if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {\n          throw new WIKI.Error.AuthRequired()\n        }\n        const usr = await WIKI.models.users.query().findById(context.req.user.id)\n        if (!usr.isActive) {\n          throw new WIKI.Error.AuthAccountBanned()\n        }\n        if (!usr.isVerified) {\n          throw new WIKI.Error.AuthAccountNotVerified()\n        }\n        if (usr.providerKey !== 'local') {\n          throw new WIKI.Error.AuthProviderInvalid()\n        }\n        try {\n          await usr.verifyPassword(args.current)\n        } catch (err) {\n          throw new WIKI.Error.AuthPasswordInvalid()\n        }\n\n        await WIKI.models.users.updateUser({\n          id: usr.id,\n          newPassword: args.new\n        })\n\n        const newToken = await WIKI.models.users.refreshToken(usr)\n\n        return {\n          responseResult: graphHelper.generateSuccess('Password changed successfully'),\n          jwt: newToken.token\n        }\n      } catch (err) {\n        return graphHelper.generateError(err)\n      }\n    }\n  },\n  User: {\n    groups (usr) {\n      return usr.$relatedQuery('groups')\n    }\n  },\n  UserProfile: {\n    async groups (usr) {\n      const usrGroups = await usr.$relatedQuery('groups')\n      return usrGroups.map(g => g.name)\n    },\n    async pagesTotal (usr) {\n      const result = await WIKI.models.pages.query().count('* as total').where('creatorId', usr.id).first()\n      return _.toSafeInteger(result.total)\n    }\n  }\n}\n"
  },
  {
    "path": "server/graph/scalars/date.js",
    "content": "\nconst gql = require('graphql')\n\nmodule.exports = {\n  Date: new gql.GraphQLScalarType({\n    name: 'Date',\n    description: 'ISO date-time string at UTC',\n    parseValue(value) {\n      return new Date(value)\n    },\n    serialize(value) {\n      return value.toISOString()\n    },\n    parseLiteral(ast) {\n      if (ast.kind !== gql.Kind.STRING) {\n        throw new TypeError('Date value must be an string!')\n      }\n      return new Date(ast.value)\n    }\n  })\n}\n"
  },
  {
    "path": "server/graph/schemas/analytics.graphql",
    "content": "# ===============================================\n# ANALYTICS\n# ===============================================\n\nextend type Query {\n  analytics: AnalyticsQuery\n}\n\nextend type Mutation {\n  analytics: AnalyticsMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\n\"\"\"\nQueries for Analytics\n\"\"\"\ntype AnalyticsQuery {\n  \"\"\"\n  Fetch list of Analytics providers and their configuration\n  \"\"\"\n  providers(\n    \"Return only active providers\"\n    isEnabled: Boolean\n  ): [AnalyticsProvider] @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\n\"\"\"\nMutations for Analytics\n\"\"\"\ntype AnalyticsMutation {\n  \"\"\"\n  Update a list of Analytics providers and their configuration\n  \"\"\"\n  updateProviders(\n    \"List of providers\"\n    providers: [AnalyticsProviderInput]!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\n\"\"\"\nAnalytics Provider\n\"\"\"\ntype AnalyticsProvider {\n  \"Is the provider active\"\n  isEnabled: Boolean!\n\n  \"Unique identifier for this provider\"\n  key: String!\n\n  \"List of configuration properties, formatted as stringified JSON objects\"\n  props: [String]\n\n  \"Name of the provider\"\n  title: String!\n\n  \"Short description of the provider\"\n  description: String\n\n  \"Is the provider available for use\"\n  isAvailable: Boolean\n\n  \"Path to the provider logo\"\n  logo: String\n\n  \"Website of the provider\"\n  website: String\n\n  \"Configuration values for this provider\"\n  config: [KeyValuePair]\n}\n\n\"\"\"\nAnalytics Configuration Input\n\"\"\"\ninput AnalyticsProviderInput {\n  \"Is the provider active\"\n  isEnabled: Boolean!\n\n  \"Unique identifier of the provider\"\n  key: String!\n\n  \"Configuration values for this provider\"\n  config: [KeyValuePairInput]\n}\n"
  },
  {
    "path": "server/graph/schemas/asset.graphql",
    "content": "# ===============================================\n# ASSETS\n# ===============================================\n\nextend type Query {\n  assets: AssetQuery\n}\n\nextend type Mutation {\n  assets: AssetMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype AssetQuery {\n  list(\n    folderId: Int!\n    kind: AssetKind!\n  ): [AssetItem] @auth(requires: [\"manage:system\", \"read:assets\"])\n\n  folders(\n    parentFolderId: Int!\n  ): [AssetFolder] @auth(requires: [\"manage:system\", \"read:assets\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype AssetMutation {\n  createFolder(\n    parentFolderId: Int!\n    slug: String!\n    name: String\n  ): DefaultResponse @auth(requires: [\"manage:system\", \"write:assets\"])\n\n  renameAsset(\n    id: Int!\n    filename: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\", \"manage:assets\"])\n\n  deleteAsset(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:system\", \"manage:assets\"])\n\n  flushTempUploads: DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype AssetItem {\n  id: Int!\n  filename: String!\n  ext: String!\n  kind: AssetKind!\n  mime: String!\n  fileSize: Int!\n  metadata: String\n  createdAt: Date!\n  updatedAt: Date!\n  folder: AssetFolder\n  author: User\n}\n\ntype AssetFolder {\n  id: Int!\n  slug: String!\n  name: String\n}\n\nenum AssetKind {\n  IMAGE\n  BINARY\n  ALL\n}\n"
  },
  {
    "path": "server/graph/schemas/authentication.graphql",
    "content": "# ===============================================\n# AUTHENTICATION\n# ===============================================\n\nextend type Query {\n  authentication: AuthenticationQuery\n}\n\nextend type Mutation {\n  authentication: AuthenticationMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype AuthenticationQuery {\n  apiKeys: [AuthenticationApiKey] @auth(requires: [\"manage:system\", \"manage:api\"])\n\n  apiState: Boolean! @auth(requires: [\"manage:system\", \"manage:api\"])\n\n  strategies: [AuthenticationStrategy] @auth(requires: [\"manage:system\"])\n\n  activeStrategies(\n    enabledOnly: Boolean\n  ): [AuthenticationActiveStrategy]\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype AuthenticationMutation {\n  createApiKey(\n    name: String!\n    expiration: String!\n    fullAccess: Boolean!\n    group: Int\n  ): AuthenticationCreateApiKeyResponse @auth(requires: [\"manage:system\", \"manage:api\"])\n\n  login(\n    username: String!\n    password: String!\n    strategy: String!\n  ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)\n\n  loginTFA(\n    continuationToken: String!\n    securityCode: String!\n    setup: Boolean\n  ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)\n\n  loginChangePassword(\n    continuationToken: String!\n    newPassword: String!\n  ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)\n\n  forgotPassword(\n    email: String!\n  ): DefaultResponse @rateLimit(limit: 3, duration: 60)\n\n  register(\n    email: String!\n    password: String!\n    name: String!\n  ): AuthenticationRegisterResponse\n\n  revokeApiKey(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:system\", \"manage:api\"])\n\n  setApiState(\n    enabled: Boolean!\n  ): DefaultResponse @auth(requires: [\"manage:system\", \"manage:api\"])\n\n  updateStrategies(\n    strategies: [AuthenticationStrategyInput]!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  regenerateCertificates: DefaultResponse @auth(requires: [\"manage:system\"])\n\n  resetGuestUser: DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype AuthenticationStrategy {\n  key: String!\n  props: [KeyValuePair] @auth(requires: [\"manage:system\"])\n  title: String!\n  description: String\n  isAvailable: Boolean\n  useForm: Boolean!\n  usernameType: String\n  logo: String\n  color: String\n  website: String\n  icon: String\n}\n\ntype AuthenticationActiveStrategy {\n  key: String!\n  strategy: AuthenticationStrategy!\n  displayName: String!\n  order: Int!\n  isEnabled: Boolean!\n  config: [KeyValuePair] @auth(requires: [\"manage:system\"])\n  selfRegistration: Boolean!\n  domainWhitelist: [String]! @auth(requires: [\"manage:system\"])\n  autoEnrollGroups: [Int]! @auth(requires: [\"manage:system\"])\n}\n\ntype AuthenticationLoginResponse {\n  responseResult: ResponseStatus\n  jwt: String\n  mustChangePwd: Boolean\n  mustProvideTFA: Boolean\n  mustSetupTFA: Boolean\n  continuationToken: String\n  redirect: String\n  tfaQRImage: String\n}\n\ntype AuthenticationRegisterResponse {\n  responseResult: ResponseStatus\n  jwt: String\n}\n\ninput AuthenticationStrategyInput {\n  key: String!\n  strategyKey: String!\n  config: [KeyValuePairInput]\n  displayName: String!\n  order: Int!\n  isEnabled: Boolean!\n  selfRegistration: Boolean!\n  domainWhitelist: [String]!\n  autoEnrollGroups: [Int]!\n}\n\ntype AuthenticationApiKey {\n  id: Int!\n  name: String!\n  keyShort: String!\n  expiration: Date!\n  createdAt: Date!\n  updatedAt: Date!\n  isRevoked: Boolean!\n}\n\ntype AuthenticationCreateApiKeyResponse {\n  responseResult: ResponseStatus\n  key: String\n}\n"
  },
  {
    "path": "server/graph/schemas/comment.graphql",
    "content": "# ===============================================\n# COMMENT\n# ===============================================\n\nextend type Query {\n  comments: CommentQuery\n}\n\nextend type Mutation {\n  comments: CommentMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype CommentQuery {\n  providers: [CommentProvider] @auth(requires: [\"manage:system\"])\n\n  list(\n    locale: String!\n    path: String!\n  ): [CommentPost]! @auth(requires: [\"read:comments\", \"manage:system\"])\n\n  single(\n    id: Int!\n  ): CommentPost @auth(requires: [\"read:comments\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype CommentMutation {\n  updateProviders(\n    providers: [CommentProviderInput]\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  create(\n    pageId: Int!\n    replyTo: Int\n    content: String!\n    guestName: String\n    guestEmail: String\n  ): CommentCreateResponse @auth(requires: [\"write:comments\", \"manage:system\"]) @rateLimit(limit: 1, duration: 15)\n\n  update(\n    id: Int!\n    content: String!\n  ): CommentUpdateResponse @auth(requires: [\"write:comments\", \"manage:comments\", \"manage:system\"])\n\n  delete(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:comments\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype CommentProvider {\n  isEnabled: Boolean!\n  key: String!\n  title: String!\n  description: String\n  logo: String\n  website: String\n  isAvailable: Boolean\n  config: [KeyValuePair]\n}\n\ninput CommentProviderInput {\n  isEnabled: Boolean!\n  key: String!\n  config: [KeyValuePairInput]\n}\n\ntype CommentPost {\n  id: Int!\n  content: String! @auth(requires: [\"write:comments\", \"manage:comments\", \"manage:system\"])\n  render: String!\n  authorId: Int!\n  authorName: String!\n  authorEmail: String! @auth(requires: [\"manage:system\"])\n  authorIP: String! @auth(requires: [\"manage:system\"])\n  createdAt: Date!\n  updatedAt: Date!\n}\n\ntype CommentCreateResponse {\n  responseResult: ResponseStatus\n  id: Int\n}\n\ntype CommentUpdateResponse {\n  responseResult: ResponseStatus\n  render: String\n}\n"
  },
  {
    "path": "server/graph/schemas/common.graphql",
    "content": "# ====================== #\n# Wiki.js GraphQL Schema #\n# ====================== #\n\n# DIRECTIVES\n# ----------\n\ndirective @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION\n\n# TYPES\n# -----\n\n# Generic Key Value Pair\ntype KeyValuePair {\n  key: String!\n  value: String!\n}\n# General Key Value Pair Input\ninput KeyValuePairInput {\n  key: String!\n  value: String!\n}\n\n# Generic Mutation Response\ntype DefaultResponse {\n  responseResult: ResponseStatus\n}\n\n# Mutation Status\ntype ResponseStatus {\n  succeeded: Boolean!\n  errorCode: Int!\n  slug: String!\n  message: String\n}\n\n# ROOT\n# ----\n\n# Query (Read)\ntype Query\n\n# Mutations (Create, Update, Delete)\ntype Mutation\n\n# Subscriptions (Push, Real-time)\ntype Subscription\n"
  },
  {
    "path": "server/graph/schemas/contribute.graphql",
    "content": "# ===============================================\n# CONTRIBUTE\n# ===============================================\n\nextend type Query {\n  contribute: ContributeQuery\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype ContributeQuery {\n  contributors: [ContributeContributor]\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype ContributeContributor {\n  id: String!\n  source: String!\n  name: String!\n  joined: Date!\n  website: String\n  twitter: String\n  avatar: String\n}\n"
  },
  {
    "path": "server/graph/schemas/group.graphql",
    "content": "# ===============================================\n# GROUPS\n# ===============================================\n\nextend type Query {\n  groups: GroupQuery\n}\n\nextend type Mutation {\n  groups: GroupMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype GroupQuery {\n  list(\n    filter: String\n    orderBy: String\n  ): [GroupMinimal] @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n\n  single(\n    id: Int!\n  ): Group @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype GroupMutation {\n  create(\n    name: String!\n  ): GroupResponse @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n\n  update(\n    id: Int!\n    name: String!\n    redirectOnLogin: String!\n    permissions: [String]!\n    pageRules: [PageRuleInput]!\n  ): DefaultResponse @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n\n  delete(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n\n  assignUser(\n    groupId: Int!\n    userId: Int!\n  ): DefaultResponse @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n\n  unassignUser(\n    groupId: Int!\n    userId: Int!\n  ): DefaultResponse @auth(requires: [\"write:groups\", \"manage:groups\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype GroupResponse {\n  responseResult: ResponseStatus!\n  group: Group\n}\n\ntype GroupMinimal {\n  id: Int!\n  name: String!\n  isSystem: Boolean!\n  userCount: Int\n  createdAt: Date!\n  updatedAt: Date!\n}\n\ntype Group {\n  id: Int!\n  name: String!\n  isSystem: Boolean!\n  redirectOnLogin: String\n  permissions: [String]!\n  pageRules: [PageRule]\n  users: [UserMinimal]\n  createdAt: Date!\n  updatedAt: Date!\n}\n\ntype PageRule {\n  id: String!\n  deny: Boolean!\n  match: PageRuleMatch!\n  roles: [String]!\n  path: String!\n  locales: [String]!\n}\n\ninput PageRuleInput {\n  id: String!\n  deny: Boolean!\n  match: PageRuleMatch!\n  roles: [String]!\n  path: String!\n  locales: [String]!\n}\n\nenum PageRuleMatch {\n  START\n  EXACT\n  END\n  REGEX\n  TAG\n}\n"
  },
  {
    "path": "server/graph/schemas/localization.graphql",
    "content": "# ===============================================\n# LOCALIZATION\n# ===============================================\n\nextend type Query {\n  localization: LocalizationQuery\n}\n\nextend type Mutation {\n  localization: LocalizationMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype LocalizationQuery {\n  locales: [LocalizationLocale]\n  config: LocalizationConfig\n  translations(locale: String!, namespace: String!): [Translation]\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype LocalizationMutation {\n  downloadLocale(\n    locale: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  updateLocale(\n    locale: String!\n    autoUpdate: Boolean!\n    namespacing: Boolean!\n    namespaces: [String]!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype LocalizationLocale {\n  availability: Int!\n  code: String!\n  createdAt: Date!\n  installDate: Date\n  isInstalled: Boolean!\n  isRTL: Boolean!\n  name: String!\n  nativeName: String!\n  updatedAt: Date!\n}\n\ntype LocalizationConfig {\n  locale: String!\n  autoUpdate: Boolean!\n  namespacing: Boolean!\n  namespaces: [String]!\n}\n\ntype Translation {\n  key: String!\n  value: String!\n}\n"
  },
  {
    "path": "server/graph/schemas/logging.graphql",
    "content": "# ===============================================\n# LOGGING\n# ===============================================\n\nextend type Query {\n  logging: LoggingQuery\n}\n\nextend type Mutation {\n  logging: LoggingMutation\n}\n\nextend type Subscription {\n  loggingLiveTrail: LoggerTrailLine\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype LoggingQuery {\n  loggers(\n    filter: String\n    orderBy: String\n  ): [Logger] @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype LoggingMutation {\n  updateLoggers(\n    loggers: [LoggerInput]\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype Logger {\n  isEnabled: Boolean!\n  key: String!\n  title: String!\n  description: String\n  logo: String\n  website: String\n  level: String\n  config: [KeyValuePair]\n}\n\ninput LoggerInput {\n  isEnabled: Boolean!\n  key: String!\n  level: String!\n  config: [KeyValuePairInput]\n}\n\ntype LoggerTrailLine {\n  level: String!\n  output: String!\n  timestamp: Date!\n}\n"
  },
  {
    "path": "server/graph/schemas/mail.graphql",
    "content": "# ===============================================\n# MAIL\n# ===============================================\n\nextend type Query {\n  mail: MailQuery\n}\n\nextend type Mutation {\n  mail: MailMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype MailQuery {\n  config: MailConfig @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype MailMutation {\n  sendTest(\n    recipientEmail: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  updateConfig(\n    senderName: String!\n    senderEmail: String!\n    host: String!\n    port: Int!\n    name: String!\n    secure: Boolean!\n    verifySSL: Boolean!\n    user: String!\n    pass: String!\n    useDKIM: Boolean!\n    dkimDomainName: String!\n    dkimKeySelector: String!\n    dkimPrivateKey: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype MailConfig {\n  senderName: String\n  senderEmail: String\n  host: String\n  port: Int\n  name: String\n  secure: Boolean\n  verifySSL: Boolean\n  user: String\n  pass: String\n  useDKIM: Boolean\n  dkimDomainName: String\n  dkimKeySelector: String\n  dkimPrivateKey: String\n}\n"
  },
  {
    "path": "server/graph/schemas/navigation.graphql",
    "content": "# ===============================================\n# NAVIGATION\n# ===============================================\n\nextend type Query {\n  navigation: NavigationQuery\n}\n\nextend type Mutation {\n  navigation: NavigationMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype NavigationQuery {\n  tree: [NavigationTree]! @auth(requires: [\"manage:navigation\", \"manage:system\"])\n  config: NavigationConfig! @auth(requires: [\"manage:navigation\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype NavigationMutation {\n  updateTree(\n    tree: [NavigationTreeInput]!\n  ): DefaultResponse @auth(requires: [\"manage:navigation\", \"manage:system\"])\n  updateConfig(\n    mode: NavigationMode!\n  ): DefaultResponse @auth(requires: [\"manage:navigation\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype NavigationTree {\n  locale: String!\n  items: [NavigationItem]!\n}\n\ninput NavigationTreeInput {\n  locale: String!\n  items: [NavigationItemInput]!\n}\n\ntype NavigationItem {\n  id: String!\n  kind: String!\n  label: String\n  icon: String\n  targetType: String\n  target: String\n  visibilityMode: String\n  visibilityGroups: [Int]\n}\n\ninput NavigationItemInput {\n  id: String!\n  kind: String!\n  label: String\n  icon: String\n  targetType: String\n  target: String\n  visibilityMode: String\n  visibilityGroups: [Int]\n}\n\ntype NavigationConfig {\n  mode: NavigationMode!\n}\n\nenum NavigationMode {\n  NONE\n  TREE\n  MIXED\n  STATIC\n}\n"
  },
  {
    "path": "server/graph/schemas/page.graphql",
    "content": "# ===============================================\n# PAGES\n# ===============================================\n\nextend type Query {\n  pages: PageQuery\n}\n\nextend type Mutation {\n  pages: PageMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype PageQuery {\n  history(\n    id: Int!\n    offsetPage: Int\n    offsetSize: Int\n  ): PageHistoryResult @auth(requires: [\"manage:system\", \"read:history\"])\n\n  version(\n    pageId: Int!\n    versionId: Int!\n  ): PageVersion @auth(requires: [\"manage:system\", \"read:history\"])\n\n  search(\n    query: String!\n    path: String\n    locale: String\n  ): PageSearchResponse! @auth(requires: [\"manage:system\", \"read:pages\"])\n\n  list(\n    limit: Int\n    orderBy: PageOrderBy\n    orderByDirection: PageOrderByDirection\n    tags: [String!]\n    locale: String\n    creatorId: Int\n    authorId: Int\n  ): [PageListItem!]! @auth(requires: [\"manage:system\", \"read:pages\"])\n\n  single(\n    id: Int!\n  ): Page @auth(requires: [\"read:pages\", \"manage:system\"])\n\n  singleByPath(\n    path: String!\n    locale: String!\n  ): Page @auth(requires: [\"read:pages\", \"manage:system\"])\n\n  tags: [PageTag]! @auth(requires: [\"manage:system\", \"read:pages\"])\n\n  searchTags(\n    query: String!\n  ): [String]! @auth(requires: [\"manage:system\", \"read:pages\"])\n\n  tree(\n    path: String\n    parent: Int\n    mode: PageTreeMode!\n    locale: String!\n    includeAncestors: Boolean\n  ): [PageTreeItem] @auth(requires: [\"manage:system\", \"read:pages\"])\n\n  links(\n    locale: String!\n  ): [PageLinkItem] @auth(requires: [\"manage:system\", \"read:pages\"])\n\n  checkConflicts(\n    id: Int!\n    checkoutDate: Date!\n  ): Boolean! @auth(requires: [\"write:pages\", \"manage:pages\", \"manage:system\"])\n\n  conflictLatest(\n    id: Int!\n  ): PageConflictLatest! @auth(requires: [\"write:pages\", \"manage:pages\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype PageMutation {\n  create(\n    content: String!\n    description: String!\n    editor: String!\n    isPublished: Boolean!\n    isPrivate: Boolean!\n    locale: String!\n    path: String!\n    publishEndDate: Date\n    publishStartDate: Date\n    scriptCss: String\n    scriptJs: String\n    tags: [String]!\n    title: String!\n  ): PageResponse @auth(requires: [\"write:pages\", \"manage:pages\", \"manage:system\"])\n\n  update(\n    id: Int!\n    content: String\n    description: String\n    editor: String\n    isPrivate: Boolean\n    isPublished: Boolean\n    locale: String\n    path: String\n    publishEndDate: Date\n    publishStartDate: Date\n    scriptCss: String\n    scriptJs: String\n    tags: [String]\n    title: String\n  ): PageResponse @auth(requires: [\"write:pages\", \"manage:pages\", \"manage:system\"])\n\n  convert(\n    id: Int!\n    editor: String!\n  ): DefaultResponse @auth(requires: [\"write:pages\", \"manage:pages\", \"manage:system\"])\n\n  move(\n    id: Int!\n    destinationPath: String!\n    destinationLocale: String!\n  ): DefaultResponse @auth(requires: [\"manage:pages\", \"manage:system\"])\n\n  delete(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"delete:pages\", \"manage:system\"])\n\n  deleteTag(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  updateTag(\n    id: Int!\n    tag: String!\n    title: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  flushCache: DefaultResponse @auth(requires: [\"manage:system\"])\n\n  migrateToLocale(\n    sourceLocale: String!\n    targetLocale: String!\n  ): PageMigrationResponse @auth(requires: [\"manage:system\"])\n\n  rebuildTree: DefaultResponse @auth(requires: [\"manage:system\"])\n\n  render(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  restore(\n    pageId: Int!\n    versionId: Int!\n  ): DefaultResponse @auth(requires: [\"write:pages\", \"manage:pages\", \"manage:system\"])\n\n  purgeHistory (\n    olderThan: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype PageResponse {\n  responseResult: ResponseStatus!\n  page: Page\n}\n\ntype PageMigrationResponse {\n  responseResult: ResponseStatus!\n  count: Int\n}\n\ntype Page {\n  id: Int!\n  path: String!\n  hash: String!\n  title: String!\n  description: String!\n  isPrivate: Boolean! @auth(requires: [\"write:pages\", \"manage:system\"])\n  isPublished: Boolean! @auth(requires: [\"write:pages\", \"manage:system\"])\n  privateNS: String @auth(requires: [\"write:pages\", \"manage:system\"])\n  publishStartDate: Date! @auth(requires: [\"write:pages\", \"manage:system\"])\n  publishEndDate: Date! @auth(requires: [\"write:pages\", \"manage:system\"])\n  tags: [PageTag]!\n  content: String! @auth(requires: [\"read:source\", \"write:pages\", \"manage:system\"])\n  render: String\n  toc: String\n  contentType: String!\n  createdAt: Date!\n  updatedAt: Date!\n  editor: String! @auth(requires: [\"write:pages\", \"manage:system\"])\n  locale: String!\n  scriptCss: String\n  scriptJs: String\n  authorId: Int! @auth(requires: [\"write:pages\", \"manage:system\"])\n  authorName: String! @auth(requires: [\"write:pages\", \"manage:system\"])\n  authorEmail: String! @auth(requires: [\"write:pages\", \"manage:system\"])\n  creatorId: Int! @auth(requires: [\"write:pages\", \"manage:system\"])\n  creatorName: String! @auth(requires: [\"write:pages\", \"manage:system\"])\n  creatorEmail: String! @auth(requires: [\"write:pages\", \"manage:system\"])\n}\n\ntype PageTag {\n  id: Int!\n  tag: String!\n  title: String\n  createdAt: Date!\n  updatedAt: Date!\n}\n\ntype PageHistory {\n  versionId: Int!\n  versionDate: Date!\n  authorId: Int!\n  authorName: String!\n  actionType: String!\n  valueBefore: String\n  valueAfter: String\n}\n\ntype PageVersion {\n  action: String!\n  authorId: String!\n  authorName: String!\n  content: String!\n  contentType: String!\n  createdAt: Date!\n  versionDate: Date!\n  description: String!\n  editor: String!\n  isPrivate: Boolean!\n  isPublished: Boolean!\n  locale: String!\n  pageId: Int!\n  path: String!\n  publishEndDate: Date!\n  publishStartDate: Date!\n  tags: [String]!\n  title: String!\n  versionId: Int!\n}\n\ntype PageHistoryResult {\n  trail: [PageHistory]\n  total: Int!\n}\n\ntype PageSearchResponse {\n  results: [PageSearchResult]!\n  suggestions: [String]!\n  totalHits: Int!\n}\n\ntype PageSearchResult {\n  id: String!\n  title: String!\n  description: String!\n  path: String!\n  locale: String!\n}\n\ntype PageListItem {\n  id: Int!\n  path: String!\n  locale: String!\n  title: String\n  description: String\n  contentType: String!\n  isPublished: Boolean!\n  isPrivate: Boolean!\n  privateNS: String\n  createdAt: Date!\n  updatedAt: Date!\n  tags: [String]\n}\n\ntype PageTreeItem {\n  id: Int!\n  path: String!\n  depth: Int!\n  title: String!\n  isPrivate: Boolean!\n  isFolder: Boolean!\n  privateNS: String\n  parent: Int\n  pageId: Int\n  locale: String!\n}\n\ntype PageLinkItem {\n  id: Int!\n  path: String!\n  title: String!\n  links: [String]!\n}\n\ntype PageConflictLatest {\n  id: Int!\n  authorId: String!\n  authorName: String!\n  content: String!\n  createdAt: Date!\n  description: String!\n  isPublished: Boolean!\n  locale: String!\n  path: String!\n  tags: [String]\n  title: String!\n  updatedAt: Date!\n}\n\nenum PageOrderBy {\n  CREATED\n  ID\n  PATH\n  TITLE\n  UPDATED\n}\n\nenum PageOrderByDirection {\n  ASC\n  DESC\n}\n\nenum PageTreeMode {\n  FOLDERS\n  PAGES\n  ALL\n}\n"
  },
  {
    "path": "server/graph/schemas/rendering.graphql",
    "content": "# ===============================================\n# RENDERING\n# ===============================================\n\nextend type Query {\n  rendering: RenderingQuery\n}\n\nextend type Mutation {\n  rendering: RenderingMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype RenderingQuery {\n  renderers(\n    filter: String\n    orderBy: String\n  ): [Renderer] @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype RenderingMutation {\n  updateRenderers(\n    renderers: [RendererInput]\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype Renderer {\n  isEnabled: Boolean!\n  key: String!\n  title: String!\n  description: String\n  icon: String\n  dependsOn: String\n  input: String\n  output: String\n  config: [KeyValuePair]\n}\n\ninput RendererInput {\n  isEnabled: Boolean!\n  key: String!\n  config: [KeyValuePairInput]\n}\n"
  },
  {
    "path": "server/graph/schemas/scalars.graphql",
    "content": "# SCALARS\n\nscalar Date\n"
  },
  {
    "path": "server/graph/schemas/search.graphql",
    "content": "# ===============================================\n# SEARCH\n# ===============================================\n\nextend type Query {\n  search: SearchQuery\n}\n\nextend type Mutation {\n  search: SearchMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype SearchQuery {\n  searchEngines(\n    filter: String\n    orderBy: String\n  ): [SearchEngine] @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype SearchMutation {\n  updateSearchEngines(\n    engines: [SearchEngineInput]\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  rebuildIndex: DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype SearchEngine {\n  isEnabled: Boolean!\n  key: String!\n  title: String!\n  description: String\n  logo: String\n  website: String\n  isAvailable: Boolean\n  config: [KeyValuePair]\n}\n\ninput SearchEngineInput {\n  isEnabled: Boolean!\n  key: String!\n  config: [KeyValuePairInput]\n}\n"
  },
  {
    "path": "server/graph/schemas/site.graphql",
    "content": "# ===============================================\n# SITE\n# ===============================================\n\nextend type Query {\n  site: SiteQuery\n}\n\nextend type Mutation {\n  site: SiteMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype SiteQuery {\n  config: SiteConfig @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype SiteMutation {\n  updateConfig(\n    host: String\n    title: String\n    description: String\n    robots: [String]\n    analyticsService: String\n    analyticsId: String\n    company: String\n    contentLicense: String\n    footerOverride: String\n    logoUrl: String\n    pageExtensions: String\n    authAutoLogin: Boolean\n    authEnforce2FA: Boolean\n    authHideLocal: Boolean\n    authLoginBgUrl: String\n    authJwtAudience: String\n    authJwtExpiration: String\n    authJwtRenewablePeriod: String\n    editFab: Boolean\n    editMenuBar: Boolean\n    editMenuBtn: Boolean\n    editMenuExternalBtn: Boolean\n    editMenuExternalName: String\n    editMenuExternalIcon: String\n    editMenuExternalUrl: String\n    featurePageRatings: Boolean\n    featurePageComments: Boolean\n    featurePersonalWikis: Boolean\n    securityOpenRedirect: Boolean\n    securityIframe: Boolean\n    securityReferrerPolicy: Boolean\n    securityTrustProxy: Boolean\n    securitySRI: Boolean\n    securityHSTS: Boolean\n    securityHSTSDuration: Int\n    securityCSP: Boolean\n    securityCSPDirectives: String\n    uploadMaxFileSize: Int\n    uploadMaxFiles: Int\n    uploadScanSVG: Boolean\n    uploadForceDownload: Boolean\n\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype SiteConfig {\n  host: String\n  title: String\n  description: String\n  robots: [String]\n  analyticsService: String\n  analyticsId: String\n  company: String\n  contentLicense: String\n  footerOverride: String\n  logoUrl: String\n  pageExtensions: String\n  authAutoLogin: Boolean\n  authEnforce2FA: Boolean\n  authHideLocal: Boolean\n  authLoginBgUrl: String\n  authJwtAudience: String\n  authJwtExpiration: String\n  authJwtRenewablePeriod: String\n  editFab: Boolean\n  editMenuBar: Boolean\n  editMenuBtn: Boolean\n  editMenuExternalBtn: Boolean\n  editMenuExternalName: String\n  editMenuExternalIcon: String\n  editMenuExternalUrl: String\n  featurePageRatings: Boolean\n  featurePageComments: Boolean\n  featurePersonalWikis: Boolean\n  securityOpenRedirect: Boolean\n  securityIframe: Boolean\n  securityReferrerPolicy: Boolean\n  securityTrustProxy: Boolean\n  securitySRI: Boolean\n  securityHSTS: Boolean\n  securityHSTSDuration: Int\n  securityCSP: Boolean\n  securityCSPDirectives: String\n  uploadMaxFileSize: Int\n  uploadMaxFiles: Int\n  uploadScanSVG: Boolean\n  uploadForceDownload: Boolean\n}\n"
  },
  {
    "path": "server/graph/schemas/storage.graphql",
    "content": "# ===============================================\n# STORAGE\n# ===============================================\n\nextend type Query {\n  storage: StorageQuery\n}\n\nextend type Mutation {\n  storage: StorageMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype StorageQuery {\n  targets: [StorageTarget] @auth(requires: [\"manage:system\"])\n  status: [StorageStatus] @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype StorageMutation {\n  updateTargets(\n    targets: [StorageTargetInput]!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  executeAction(\n    targetKey: String!\n    handler: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype StorageTarget {\n  isAvailable: Boolean!\n  isEnabled: Boolean!\n  key: String!\n  title: String!\n  description: String\n  logo: String\n  website: String\n  supportedModes: [String]\n  mode: String\n  hasSchedule: Boolean!\n  syncInterval: String\n  syncIntervalDefault: String\n  config: [KeyValuePair]\n  actions: [StorageTargetAction]\n}\n\ninput StorageTargetInput {\n  isEnabled: Boolean!\n  key: String!\n  mode: String!\n  syncInterval: String\n  config: [KeyValuePairInput]\n}\n\ntype StorageStatus {\n  key: String!\n  title: String!\n  status: String!\n  message: String!\n  lastAttempt: String!\n}\n\ntype StorageTargetAction {\n  handler: String!\n  label: String!\n  hint: String!\n}\n"
  },
  {
    "path": "server/graph/schemas/system.graphql",
    "content": "# ===============================================\n# SYSTEM\n# ===============================================\n\nextend type Query {\n  system: SystemQuery\n}\n\nextend type Mutation {\n  system: SystemMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype SystemQuery {\n  flags: [SystemFlag] @auth(requires: [\"manage:system\"])\n  info: SystemInfo\n  extensions: [SystemExtension] @auth(requires: [\"manage:system\"])\n  exportStatus: SystemExportStatus @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype SystemMutation {\n  updateFlags(\n    flags: [SystemFlagInput]!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  resetTelemetryClientId: DefaultResponse @auth(requires: [\"manage:system\"])\n\n  setTelemetry(\n    enabled: Boolean!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  performUpgrade: DefaultResponse @auth(requires: [\"manage:system\"])\n\n  importUsersFromV1(\n    mongoDbConnString: String!\n    groupMode: SystemImportUsersGroupMode!\n  ): SystemImportUsersResponse @auth(requires:  [\"manage:system\"])\n\n  setHTTPSRedirection(\n    enabled: Boolean!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n\n  renewHTTPSCertificate: DefaultResponse @auth(requires: [\"manage:system\"])\n\n  export(\n    entities: [String]!\n    path: String!\n  ): DefaultResponse @auth(requires: [\"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype SystemFlag {\n  key: String!\n  value: Boolean!\n}\n\ninput SystemFlagInput {\n  key: String!\n  value: Boolean!\n}\n\ntype SystemInfo {\n  configFile: String @auth(requires: [\"manage:system\"])\n  cpuCores: Int @auth(requires: [\"manage:system\"])\n  currentVersion: String @auth(requires: [\"manage:system\"])\n  dbHost: String @auth(requires: [\"manage:system\"])\n  dbType: String @auth(requires: [\"manage:system\"])\n  dbVersion: String @auth(requires: [\"manage:system\"])\n  groupsTotal: Int @auth(requires: [\"manage:system\", \"manage:navigation\", \"manage:groups\", \"write:groups\", \"manage:users\", \"write:users\"])\n  hostname: String @auth(requires: [\"manage:system\"])\n  httpPort: Int @auth(requires: [\"manage:system\"])\n  httpRedirection: Boolean @auth(requires: [\"manage:system\"])\n  httpsPort: Int @auth(requires: [\"manage:system\"])\n  latestVersion: String @auth(requires: [\"manage:system\"])\n  latestVersionReleaseDate: Date @auth(requires: [\"manage:system\"])\n  nodeVersion: String @auth(requires: [\"manage:system\"])\n  operatingSystem: String @auth(requires: [\"manage:system\"])\n  pagesTotal: Int @auth(requires: [\"manage:system\", \"manage:navigation\", \"manage:pages\", \"delete:pages\"])\n  platform: String @auth(requires: [\"manage:system\"])\n  ramTotal: String @auth(requires: [\"manage:system\"])\n  sslDomain: String @auth(requires: [\"manage:system\"])\n  sslExpirationDate: Date @auth(requires: [\"manage:system\"])\n  sslProvider: String @auth(requires: [\"manage:system\"])\n  sslStatus: String @auth(requires: [\"manage:system\"])\n  sslSubscriberEmail: String @auth(requires: [\"manage:system\"])\n  tagsTotal: Int @auth(requires: [\"manage:system\", \"manage:navigation\", \"manage:pages\", \"delete:pages\"])\n  telemetry: Boolean @auth(requires: [\"manage:system\"])\n  telemetryClientId: String @auth(requires: [\"manage:system\"])\n  upgradeCapable: Boolean @auth(requires: [\"manage:system\"])\n  usersTotal: Int @auth(requires: [\"manage:system\", \"manage:navigation\", \"manage:groups\", \"write:groups\", \"manage:users\", \"write:users\"])\n  workingDirectory: String @auth(requires: [\"manage:system\"])\n}\n\nenum SystemImportUsersGroupMode {\n  MULTI\n  SINGLE\n  NONE\n}\n\ntype SystemImportUsersResponse {\n  responseResult: ResponseStatus\n  usersCount: Int\n  groupsCount: Int\n  failed: [SystemImportUsersResponseFailed]\n}\n\ntype SystemImportUsersResponseFailed {\n  provider: String\n  email: String\n  error: String\n}\n\ntype SystemExtension {\n  key: String!\n  title: String!\n  description: String!\n  isInstalled: Boolean!\n  isCompatible: Boolean!\n}\n\ntype SystemExportStatus {\n  status: String\n  progress: Int\n  message: String\n  startedAt: Date\n}\n"
  },
  {
    "path": "server/graph/schemas/theming.graphql",
    "content": "# ===============================================\n# THEMES\n# ===============================================\n\nextend type Query {\n  theming: ThemingQuery\n}\n\nextend type Mutation {\n  theming: ThemingMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype ThemingQuery {\n  themes: [ThemingTheme] @auth(requires: [\"manage:theme\", \"manage:system\"])\n  config: ThemingConfig @auth(requires: [\"manage:theme\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype ThemingMutation {\n  setConfig(\n    theme: String!\n    iconset: String!\n    darkMode: Boolean!\n    tocPosition: String\n    injectCSS: String\n    injectHead: String\n    injectBody: String\n  ): DefaultResponse @auth(requires: [\"manage:theme\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype ThemingConfig {\n  theme: String!\n  iconset: String!\n  darkMode: Boolean!\n  tocPosition: String\n  injectCSS: String\n  injectHead: String\n  injectBody: String\n}\n\ntype ThemingTheme {\n  key: String\n  title: String\n  author: String\n}\n"
  },
  {
    "path": "server/graph/schemas/user.graphql",
    "content": "# ===============================================\n# USERS\n# ===============================================\n\nextend type Query {\n  users: UserQuery\n}\n\nextend type Mutation {\n  users: UserMutation\n}\n\n# -----------------------------------------------\n# QUERIES\n# -----------------------------------------------\n\ntype UserQuery {\n  list(\n    filter: String\n    orderBy: String\n  ): [UserMinimal] @auth(requires: [\"write:users\", \"manage:users\", \"manage:system\"])\n\n  search(\n    query: String!\n  ): [UserMinimal] @auth(requires: [\"write:groups\", \"manage:groups\", \"write:users\", \"manage:users\", \"manage:system\"])\n\n  single(\n    id: Int!\n  ): User @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  profile: UserProfile\n\n  lastLogins: [UserLastLogin] @auth(requires: [\"write:groups\", \"manage:groups\", \"write:users\", \"manage:users\", \"manage:system\"])\n}\n\n# -----------------------------------------------\n# MUTATIONS\n# -----------------------------------------------\n\ntype UserMutation {\n  create(\n    email: String!\n    name: String!\n    passwordRaw: String\n    providerKey: String!\n    groups: [Int]!\n    mustChangePassword: Boolean\n    sendWelcomeEmail: Boolean\n  ): UserResponse @auth(requires: [\"write:users\", \"manage:users\", \"manage:system\"])\n\n  update(\n    id: Int!\n    email: String\n    name: String\n    newPassword: String\n    groups: [Int]\n    location: String\n    jobTitle: String\n    timezone: String\n    dateFormat: String\n    appearance: String\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  delete(\n    id: Int!\n    replaceId: Int!\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  verify(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  activate(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  deactivate(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  enableTFA(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  disableTFA(\n    id: Int!\n  ): DefaultResponse @auth(requires: [\"manage:users\", \"manage:system\"])\n\n  resetPassword(\n    id: Int!\n  ): DefaultResponse\n\n  updateProfile(\n    name: String!\n    location: String!\n    jobTitle: String!\n    timezone: String!\n    dateFormat: String!\n    appearance: String!\n  ): UserTokenResponse\n\n  changePassword(\n    current: String!\n    new: String!\n  ): UserTokenResponse\n}\n\n# -----------------------------------------------\n# TYPES\n# -----------------------------------------------\n\ntype UserResponse {\n  responseResult: ResponseStatus!\n  user: User\n}\n\ntype UserLastLogin {\n  id: Int!\n  name: String!\n  lastLoginAt: Date!\n}\n\ntype UserMinimal {\n  id: Int!\n  name: String!\n  email: String!\n  providerKey: String!\n  isSystem: Boolean!\n  isActive: Boolean!\n  createdAt: Date!\n  lastLoginAt: Date\n}\n\ntype User {\n  id: Int!\n  name: String!\n  email: String!\n  providerKey: String!\n  providerName: String\n  providerId: String\n  providerIs2FACapable: Boolean\n  isSystem: Boolean!\n  isActive: Boolean!\n  isVerified: Boolean!\n  location: String!\n  jobTitle: String!\n  timezone: String!\n  dateFormat: String!\n  appearance: String!\n  createdAt: Date!\n  updatedAt: Date!\n  lastLoginAt: Date\n  tfaIsActive: Boolean!\n  groups: [Group]!\n}\n\ntype UserProfile {\n  id: Int!\n  name: String!\n  email: String!\n  providerKey: String\n  providerName: String\n  isSystem: Boolean!\n  isVerified: Boolean!\n  location: String!\n  jobTitle: String!\n  timezone: String!\n  dateFormat: String!\n  appearance: String!\n  createdAt: Date!\n  updatedAt: Date!\n  lastLoginAt: Date\n  groups: [String]!\n  pagesTotal: Int!\n}\n\ntype UserTokenResponse {\n  responseResult: ResponseStatus!\n  jwt: String\n}\n"
  },
  {
    "path": "server/helpers/asset.js",
    "content": "const crypto = require('crypto')\nconst path = require('path')\n\nmodule.exports = {\n  /**\n   * Generate unique hash from page\n   */\n  generateHash(assetPath) {\n    return crypto.createHash('sha1').update(assetPath).digest('hex')\n  },\n\n  getPathInfo(assetPath) {\n    return path.parse(assetPath.toLowerCase())\n  }\n}\n"
  },
  {
    "path": "server/helpers/brute-knex.js",
    "content": "const AbstractClientStore = require('express-brute/lib/AbstractClientStore')\n\nconst KnexStore = module.exports = function (options) {\n  options = options || Object.create(null)\n\n  AbstractClientStore.apply(this, arguments)\n  this.options = Object.assign(Object.create(null), KnexStore.defaults, options)\n\n  if (this.options.knex) {\n    this.knex = this.options.knex\n  } else {\n    this.knex = require('knex')(KnexStore.defaultsKnex)\n  }\n\n  if (options.createTable === false) {\n    this.ready = Promise.resolve()\n  } else {\n    this.ready = this.knex.schema.hasTable(this.options.tablename)\n      .then((exists) => {\n        if (exists) {\n          return\n        }\n\n        return this.knex.schema.createTable(this.options.tablename, (table) => {\n          table.string('key')\n          table.bigInteger('firstRequest').nullable()\n          table.bigInteger('lastRequest').nullable()\n          table.bigInteger('lifetime').nullable()\n          table.integer('count')\n        })\n      })\n  }\n}\nKnexStore.prototype = Object.create(AbstractClientStore.prototype)\nKnexStore.prototype.set = async function (key, value, lifetime, callback) {\n  try {\n    lifetime = lifetime || 0\n\n    await this.ready\n    const resp = await this.knex.transaction((trx) => {\n      return trx\n        .select('*')\n        .forUpdate()\n        .from(this.options.tablename)\n        .where('key', '=', key)\n        .then((foundKeys) => {\n          if (foundKeys.length === 0) {\n            return trx.from(this.options.tablename)\n              .insert({\n                key: key,\n                lifetime: new Date(Date.now() + lifetime * 1000).getTime(),\n                lastRequest: new Date(value.lastRequest).getTime(),\n                firstRequest: new Date(value.firstRequest).getTime(),\n                count: value.count\n              })\n          } else {\n            return trx(this.options.tablename)\n              .where('key', '=', key)\n              .update({\n                lifetime: new Date(Date.now() + lifetime * 1000).getTime(),\n                count: value.count,\n                lastRequest: new Date(value.lastRequest).getTime()\n              })\n          }\n        })\n    })\n    callback(null, resp)\n  } catch (err) {\n    callback(err, null)\n  }\n}\n\nKnexStore.prototype.get = async function (key, callback) {\n  try {\n    await this.ready\n    await this.clearExpired()\n    const resp = await this.knex.select('*')\n      .from(this.options.tablename)\n      .where('key', '=', key)\n    let o = null\n\n    if (resp[0]) {\n      o = {}\n      o.lastRequest = new Date(resp[0].lastRequest)\n      o.firstRequest = new Date(resp[0].firstRequest)\n      o.count = resp[0].count\n    }\n\n    callback(null, o)\n  } catch (err) {\n    callback(err, null)\n  }\n}\nKnexStore.prototype.reset = async function (key, callback) {\n  try {\n    await this.ready\n    const resp = await this.knex(this.options.tablename)\n      .where('key', '=', key)\n      .del()\n    callback(null, resp)\n  } catch (err) {\n    callback(err, null)\n  }\n}\n\nKnexStore.prototype.increment = async function (key, lifetime, callback) {\n  try {\n    const result = await this.get(key)\n    let resp = null\n    if (result) {\n      resp = await this.knex(this.options.tablename)\n        .increment('count', 1)\n        .where('key', '=', key)\n    } else {\n      resp = await this.knex(this.options.tablename)\n        .insert({\n          key: key,\n          firstRequest: new Date().getTime(),\n          lastRequest: new Date().getTime(),\n          lifetime: new Date(Date.now() + lifetime * 1000).getTime(),\n          count: 1\n        })\n    }\n    callback(null, resp)\n  } catch (err) {\n    callback(err, null)\n  }\n}\n\nKnexStore.prototype.clearExpired = async function (callback) {\n  await this.ready\n  return this.knex(this.options.tablename)\n    .del()\n    .where('lifetime', '<', new Date().getTime())\n}\n\nKnexStore.defaults = {\n  tablename: 'brute',\n  createTable: true\n}\n\nKnexStore.defaultsKnex = {\n  client: 'sqlite3',\n  // debug: true,\n  connection: {\n    filename: './brute-knex.sqlite'\n  }\n}\n"
  },
  {
    "path": "server/helpers/common.js",
    "content": "/* global WIKI */\n\nconst _ = require('lodash')\nconst { DateTime } = require('luxon')\n\nmodule.exports = {\n  /**\n   * Get default value of type\n   *\n   * @param {any} type primitive type name\n   * @returns Default value\n   */\n  getTypeDefaultValue (type) {\n    switch (type.toLowerCase()) {\n      case 'string':\n        return ''\n      case 'number':\n        return 0\n      case 'boolean':\n        return false\n    }\n  },\n  parseModuleProps (props) {\n    return _.transform(props, (result, value, key) => {\n      let defaultValue = ''\n      if (_.isPlainObject(value)) {\n        defaultValue = !_.isNil(value.default) ? value.default : this.getTypeDefaultValue(value.type)\n      } else {\n        defaultValue = this.getTypeDefaultValue(value)\n      }\n      _.set(result, key, {\n        default: defaultValue,\n        type: (value.type || value).toLowerCase(),\n        title: value.title || _.startCase(key),\n        hint: value.hint || false,\n        enum: value.enum || false,\n        multiline: value.multiline || false,\n        sensitive: value.sensitive || false,\n        maxWidth: value.maxWidth || 0,\n        order: value.order || 100\n      })\n      return result\n    }, {})\n  },\n  getCookieOpts () {\n    return {\n      expires: DateTime.utc().plus({ days: 365 }).toJSDate(),\n      ...(WIKI.config.host.startsWith('https://') ? { secure: true } : {})\n    }\n  }\n}\n"
  },
  {
    "path": "server/helpers/config.js",
    "content": "'use strict'\n\nconst _ = require('lodash')\n\nconst isoDurationReg = /^(-|\\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/\n\nmodule.exports = {\n  /**\n   * Parse configuration value for environment vars\n   *\n   * Replaces `$(ENV_VAR_NAME)` with value of `ENV_VAR_NAME` environment variable.\n   *\n   * Also supports defaults by if provided as `$(ENV_VAR_NAME:default)`\n   *\n   * @param {any} cfg Configuration value\n   * @returns Parse configuration value\n   */\n  parseConfigValue (cfg) {\n    return _.replace(\n      cfg,\n      /\\$\\(([A-Z0-9_]+)(?::(.+))?\\)/g,\n      (fm, m, d) => { return process.env[m] || d }\n    )\n  },\n\n  isValidDurationString (val) {\n    return isoDurationReg.test(val)\n  }\n}\n"
  },
  {
    "path": "server/helpers/error.js",
    "content": "const CustomError = require('custom-error-instance')\n\nmodule.exports = {\n  AssetDeleteForbidden: CustomError('AssetDeleteForbidden', {\n    message: 'You are not authorized to delete this asset.',\n    code: 2003\n  }),\n  AssetFolderExists: CustomError('AssetFolderExists', {\n    message: 'An asset folder with the same name already exists.',\n    code: 2002\n  }),\n  AssetGenericError: CustomError('AssetGenericError', {\n    message: 'An unexpected error occured during asset operation.',\n    code: 2001\n  }),\n  AssetInvalid: CustomError('AssetInvalid', {\n    message: 'This asset does not exist or is invalid.',\n    code: 2004\n  }),\n  AssetRenameCollision: CustomError('AssetRenameCollision', {\n    message: 'An asset with the same filename in the same folder already exists.',\n    code: 2005\n  }),\n  AssetRenameForbidden: CustomError('AssetRenameForbidden', {\n    message: 'You are not authorized to rename this asset.',\n    code: 2006\n  }),\n  AssetRenameInvalid: CustomError('AssetRenameInvalid', {\n    message: 'The new asset filename is invalid.',\n    code: 2007\n  }),\n  AssetRenameInvalidExt: CustomError('AssetRenameInvalidExt', {\n    message: 'The file extension cannot be changed on an existing asset.',\n    code: 2008\n  }),\n  AssetRenameTargetForbidden: CustomError('AssetRenameTargetForbidden', {\n    message: 'You are not authorized to rename this asset to the requested name.',\n    code: 2009\n  }),\n  AuthAccountBanned: CustomError('AuthAccountBanned', {\n    message: 'Your account has been disabled.',\n    code: 1013\n  }),\n  AuthAccountAlreadyExists: CustomError('AuthAccountAlreadyExists', {\n    message: 'An account already exists using this email address.',\n    code: 1004\n  }),\n  AuthAccountNotVerified: CustomError('AuthAccountNotVerified', {\n    message: 'You must verify your account before your can login.',\n    code: 1014\n  }),\n  AuthGenericError: CustomError('AuthGenericError', {\n    message: 'An unexpected error occured during login.',\n    code: 1001\n  }),\n  AuthLoginFailed: CustomError('AuthLoginFailed', {\n    message: 'Invalid email / username or password.',\n    code: 1002\n  }),\n  AuthPasswordInvalid: CustomError('AuthPasswordInvalid', {\n    message: 'Password is incorrect.',\n    code: 1020\n  }),\n  AuthProviderInvalid: CustomError('AuthProviderInvalid', {\n    message: 'Invalid authentication provider.',\n    code: 1003\n  }),\n  AuthRegistrationDisabled: CustomError('AuthRegistrationDisabled', {\n    message: 'Registration is disabled. Contact your system administrator.',\n    code: 1010\n  }),\n  AuthRegistrationDomainUnauthorized: CustomError('AuthRegistrationDomainUnauthorized', {\n    message: 'You are not authorized to register. Your domain is not whitelisted.',\n    code: 1011\n  }),\n  AuthRequired: CustomError('AuthRequired', {\n    message: 'You must be authenticated to access this resource.',\n    code: 1019\n  }),\n  AuthTFAFailed: CustomError('AuthTFAFailed', {\n    message: 'Incorrect TFA Security Code.',\n    code: 1005\n  }),\n  AuthTFAInvalid: CustomError('AuthTFAInvalid', {\n    message: 'Invalid TFA Security Code or Login Token.',\n    code: 1006\n  }),\n  AuthValidationTokenInvalid: CustomError('AuthValidationTokenInvalid', {\n    message: 'Invalid validation token.',\n    code: 1015\n  }),\n  BruteInstanceIsInvalid: CustomError('BruteInstanceIsInvalid', {\n    message: 'Invalid Brute Force Instance.',\n    code: 1007\n  }),\n  BruteTooManyAttempts: CustomError('BruteTooManyAttempts', {\n    message: 'Too many attempts! Try again later.',\n    code: 1008\n  }),\n  CommentContentMissing: CustomError('CommentContentMissing', {\n    message: 'Comment content is missing or too short.',\n    code: 8003\n  }),\n  CommentGenericError: CustomError('CommentGenericError', {\n    message: 'An unexpected error occured.',\n    code: 8001\n  }),\n  CommentManageForbidden: CustomError('CommentManageForbidden', {\n    message: 'You are not authorized to manage comments on this page.',\n    code: 8004\n  }),\n  CommentNotFound: CustomError('CommentNotFound', {\n    message: 'This comment does not exist.',\n    code: 8005\n  }),\n  CommentPostForbidden: CustomError('CommentPostForbidden', {\n    message: 'You are not authorized to post a comment on this page.',\n    code: 8002\n  }),\n  CommentViewForbidden: CustomError('CommentViewForbidden', {\n    message: 'You are not authorized to view comments for this page.',\n    code: 8006\n  }),\n  InputInvalid: CustomError('InputInvalid', {\n    message: 'Input data is invalid.',\n    code: 1012\n  }),\n  LocaleGenericError: CustomError('LocaleGenericError', {\n    message: 'An unexpected error occured during locale operation.',\n    code: 5001\n  }),\n  LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', {\n    message: 'Invalid locale or namespace.',\n    code: 5002\n  }),\n  MailGenericError: CustomError('MailGenericError', {\n    message: 'An unexpected error occured during mail operation.',\n    code: 3001\n  }),\n  MailInvalidRecipient: CustomError('MailInvalidRecipient', {\n    message: 'The recipient email address is invalid.',\n    code: 3004\n  }),\n  MailNotConfigured: CustomError('MailNotConfigured', {\n    message: 'The mail configuration is incomplete or invalid.',\n    code: 3002\n  }),\n  MailTemplateFailed: CustomError('MailTemplateFailed', {\n    message: 'Mail template failed to load.',\n    code: 3003\n  }),\n  PageCreateForbidden: CustomError('PageCreateForbidden', {\n    message: 'You are not authorized to create this page.',\n    code: 6008\n  }),\n  PageDeleteForbidden: CustomError('PageDeleteForbidden', {\n    message: 'You are not authorized to delete this page.',\n    code: 6010\n  }),\n  PageGenericError: CustomError('PageGenericError', {\n    message: 'An unexpected error occured during a page operation.',\n    code: 6001\n  }),\n  PageDuplicateCreate: CustomError('PageDuplicateCreate', {\n    message: 'Cannot create this page because an entry already exists at the same path.',\n    code: 6002\n  }),\n  PageEmptyContent: CustomError('PageEmptyContent', {\n    message: 'Page content cannot be empty.',\n    code: 6004\n  }),\n  PageHistoryForbidden: CustomError('PageHistoryForbidden', {\n    message: 'You are not authorized to view the history of this page.',\n    code: 6012\n  }),\n  PageIllegalPath: CustomError('PageIllegalPath', {\n    message: 'Page path cannot contains illegal characters.',\n    code: 6005\n  }),\n  PageMoveForbidden: CustomError('PageMoveForbidden', {\n    message: 'You are not authorized to move this page.',\n    code: 6007\n  }),\n  PageNotFound: CustomError('PageNotFound', {\n    message: 'This page does not exist.',\n    code: 6003\n  }),\n  PagePathCollision: CustomError('PagePathCollision', {\n    message: 'Destination page path already exists.',\n    code: 6006\n  }),\n  PageRestoreForbidden: CustomError('PageRestoreForbidden', {\n    message: 'You are not authorized to restore this page version.',\n    code: 6011\n  }),\n  PageUpdateForbidden: CustomError('PageUpdateForbidden', {\n    message: 'You are not authorized to update this page.',\n    code: 6009\n  }),\n  PageViewForbidden: CustomError('PageViewForbidden', {\n    message: 'You are not authorized to view this page.',\n    code: 6013\n  }),\n  SearchActivationFailed: CustomError('SearchActivationFailed', {\n    message: 'Search Engine activation failed.',\n    code: 4002\n  }),\n  SearchGenericError: CustomError('SearchGenericError', {\n    message: 'An unexpected error occured during search operation.',\n    code: 4001\n  }),\n  SystemGenericError: CustomError('SystemGenericError', {\n    message: 'An unexpected error occured.',\n    code: 7001\n  }),\n  SystemSSLDisabled: CustomError('SystemSSLDisabled', {\n    message: 'SSL is not enabled.',\n    code: 7002\n  }),\n  SystemSSLLEUnavailable: CustomError('SystemSSLLEUnavailable', {\n    message: 'Let\\'s Encrypt is not initialized.',\n    code: 7004\n  }),\n  SystemSSLRenewInvalidProvider: CustomError('SystemSSLRenewInvalidProvider', {\n    message: 'Current provider does not support SSL certificate renewal.',\n    code: 7003\n  }),\n  UserCreationFailed: CustomError('UserCreationFailed', {\n    message: 'An unexpected error occured during user creation.',\n    code: 1009\n  }),\n  UserDeleteForeignConstraint: CustomError('UserDeleteForeignConstraint', {\n    message: 'Cannot delete user because of content relational constraints.',\n    code: 1017\n  }),\n  UserDeleteProtected: CustomError('UserDeleteProtected', {\n    message: 'Cannot delete a protected system account.',\n    code: 1018\n  }),\n  UserNotFound: CustomError('UserNotFound', {\n    message: 'This user does not exist.',\n    code: 1016\n  })\n}\n"
  },
  {
    "path": "server/helpers/graph.js",
    "content": "const _ = require('lodash')\n\nmodule.exports = {\n  generateSuccess (msg) {\n    return {\n      succeeded: true,\n      errorCode: 0,\n      slug: 'ok',\n      message: _.defaultTo(msg, 'Operation succeeded.')\n    }\n  },\n  generateError (err, complete = true) {\n    const error = {\n      succeeded: false,\n      errorCode: _.isFinite(err.code) ? err.code : 1,\n      slug: err.name,\n      message: err.message || 'An unexpected error occured.'\n    }\n    return (complete) ? { responseResult: error } : error\n  }\n}\n"
  },
  {
    "path": "server/helpers/page.js",
    "content": "const qs = require('querystring')\nconst _ = require('lodash')\nconst crypto = require('crypto')\nconst path = require('path')\n\nconst localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i\nconst localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\\/)?(.*)/i\n// eslint-disable-next-line no-control-regex\nconst unsafeCharsRegex = /[\\x00-\\x1f\\x80-\\x9f\\\\\"|<>:*?]/\n\nconst contentToExt = {\n  markdown: 'md',\n  asciidoc: 'adoc',\n  html: 'html'\n}\nconst extToContent = _.invert(contentToExt)\n\n/* global WIKI */\n\nmodule.exports = {\n  /**\n   * Parse raw url path and make it safe\n   */\n  parsePath (rawPath, opts = {}) {\n    let pathObj = {\n      locale: WIKI.config.lang.code,\n      path: 'home',\n      private: false,\n      privateNS: '',\n      explicitLocale: false\n    }\n\n    // Clean Path\n    rawPath = _.trim(qs.unescape(rawPath))\n    if (_.startsWith(rawPath, '/')) { rawPath = rawPath.substring(1) }\n    rawPath = rawPath.replace(unsafeCharsRegex, '')\n    if (rawPath === '') { rawPath = 'home' }\n\n    rawPath = rawPath.replace(/\\\\/g, '').replace(/\\/\\//g, '').replace(/\\.\\.+/ig, '')\n\n    // Extract Info\n    let pathParts = _.filter(_.split(rawPath, '/'), p => {\n      p = _.trim(p)\n      return !_.isEmpty(p) && p !== '..' && p !== '.'\n    })\n    if (pathParts[0].length === 1) {\n      pathParts.shift()\n    }\n    if (localeSegmentRegex.test(pathParts[0])) {\n      pathObj.locale = pathParts[0]\n      pathObj.explicitLocale = true\n      pathParts.shift()\n    }\n\n    // Strip extension\n    if (opts.stripExt && pathParts.length > 0) {\n      const lastPart = _.last(pathParts)\n      if (lastPart.indexOf('.') > 0) {\n        pathParts.pop()\n        const lastPartMeta = path.parse(lastPart)\n        pathParts.push(lastPartMeta.name)\n      }\n    }\n\n    pathObj.path = _.join(pathParts, '/')\n    return pathObj\n  },\n  /**\n   * Generate unique hash from page\n   */\n  generateHash(opts) {\n    return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')\n  },\n  /**\n   * Inject Page Metadata\n   */\n  injectPageMetadata(page) {\n    let meta = [\n      ['title', page.title],\n      ['description', page.description],\n      ['published', page.isPublished.toString()],\n      ['date', page.updatedAt],\n      ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],\n      ['editor', page.editorKey],\n      ['dateCreated', page.createdAt]\n    ]\n    switch (page.contentType) {\n      case 'markdown':\n        return '---\\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\\n') + '\\n---\\n\\n' + page.content\n      case 'html':\n        return '<!--\\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\\n') + '\\n-->\\n\\n' + page.content\n      case 'json':\n        return {\n          ...page.content,\n          _meta: _.fromPairs(meta)\n        }\n      default:\n        return page.content\n    }\n  },\n  /**\n   * Check if path is a reserved path\n   */\n  isReservedPath(rawPath) {\n    const firstSection = _.head(rawPath.split('/'))\n    if (firstSection.length <= 1) {\n      return true\n    } else if (localeSegmentRegex.test(firstSection)) {\n      return true\n    } else if (\n      _.some(WIKI.data.reservedPaths, p => {\n        return p === firstSection\n      })) {\n      return true\n    } else {\n      return false\n    }\n  },\n  /**\n   * Get file extension from content type\n   */\n  getFileExtension(contentType) {\n    return _.get(contentToExt, contentType, 'txt')\n  },\n  /**\n   * Get content type from file extension\n   */\n  getContentType (filePath) {\n    const ext = _.last(filePath.split('.'))\n    return _.get(extToContent, ext, false)\n  },\n  /**\n   * Get Page Meta object from disk path\n   */\n  getPagePath (filePath) {\n    let fpath = filePath\n    if (process.platform === 'win32') {\n      fpath = filePath.replace(/\\\\/g, '/')\n    }\n    let meta = {\n      locale: WIKI.config.lang.code,\n      path: _.initial(fpath.split('.')).join('')\n    }\n    const result = localeFolderRegex.exec(meta.path)\n    if (result[1]) {\n      meta = {\n        locale: result[1].replace('/', ''),\n        path: result[2]\n      }\n    }\n    return meta\n  }\n}\n"
  },
  {
    "path": "server/helpers/security.js",
    "content": "const Promise = require('bluebird')\nconst crypto = require('crypto')\nconst passportJWT = require('passport-jwt')\n\nmodule.exports = {\n  sanitizeCommitUser (user) {\n    // let wlist = new RegExp('[^a-zA-Z0-9-_.\\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')\n    // return {\n    //   name: _.chain(user.name).replace(wlist, '').trim().value(),\n    //   email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail\n    // }\n  },\n  /**\n   * Generate a random token\n   *\n   * @param {any} length\n   * @returns\n   */\n  async generateToken (length) {\n    return Promise.fromCallback(clb => {\n      crypto.randomBytes(length, clb)\n    }).then(buf => {\n      return buf.toString('hex')\n    })\n  },\n\n  extractJWT: passportJWT.ExtractJwt.fromExtractors([\n    passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(),\n    (req) => {\n      let token = null\n      if (req && req.cookies) {\n        token = req.cookies['jwt']\n      }\n      // Force uploads to use Auth headers\n      if (req.path.toLowerCase() === '/u') {\n        return null\n      }\n      return token\n    }\n  ])\n}\n"
  },
  {
    "path": "server/index.js",
    "content": "// ===========================================\n// Wiki.js\n// Licensed under AGPLv3\n// ===========================================\n\nconst path = require('path')\nconst { nanoid } = require('nanoid')\nconst { DateTime } = require('luxon')\nconst { gte } = require('semver')\n\n// ----------------------------------------\n// Init WIKI instance\n// ----------------------------------------\n\nlet WIKI = {\n  IS_DEBUG: process.env.NODE_ENV === 'development',\n  IS_MASTER: true,\n  ROOTPATH: process.cwd(),\n  INSTANCE_ID: nanoid(10),\n  SERVERPATH: path.join(process.cwd(), 'server'),\n  Error: require('./helpers/error'),\n  configSvc: require('./core/config'),\n  kernel: require('./core/kernel'),\n  startedAt: DateTime.utc()\n}\nglobal.WIKI = WIKI\n\nWIKI.configSvc.init()\n\n// ----------------------------------------\n// Init Logger\n// ----------------------------------------\n\nWIKI.logger = require('./core/logger').init('MASTER')\n\n// ----------------------------------------\n// Start Kernel\n// ----------------------------------------\n\nWIKI.kernel.init()\n\n// ----------------------------------------\n// Register exit handler\n// ----------------------------------------\n\nprocess.on('SIGTERM', () => {\n  WIKI.kernel.shutdown()\n})\nprocess.on('SIGINT', () => {\n  WIKI.kernel.shutdown()\n})\nprocess.on('message', (msg) => {\n  if (msg === 'shutdown') {\n    WIKI.kernel.shutdown()\n  }\n})\n"
  },
  {
    "path": "server/jobs/fetch-graph-locale.js",
    "content": "const _ = require('lodash')\nconst { createApolloFetch } = require('apollo-fetch')\n\n/* global WIKI */\n\nmodule.exports = async (localeCode) => {\n  WIKI.logger.info(`Fetching locale ${localeCode} from Graph endpoint...`)\n\n  try {\n    const apollo = createApolloFetch({\n      uri: WIKI.config.graphEndpoint\n    })\n\n    const respStrings = await apollo({\n      query: `query ($code: String!) {\n        localization {\n          strings(code: $code) {\n            key\n            value\n          }\n        }\n      }`,\n      variables: {\n        code: localeCode\n      }\n    })\n    const strings = _.get(respStrings, 'data.localization.strings', [])\n    let lcObj = {}\n    _.forEach(strings, row => {\n      if (_.includes(row.key, '::')) { return }\n      if (_.isEmpty(row.value)) {\n        row.value = row.key\n      }\n      _.set(lcObj, row.key.replace(':', '.'), row.value)\n    })\n\n    const locales = await WIKI.cache.get('locales')\n    if (locales) {\n      const currentLocale = _.find(locales, ['code', localeCode]) || {}\n      const existingLocale = await WIKI.models.locales.query().where('code', localeCode).first()\n      if (existingLocale) {\n        await WIKI.models.locales.query().patch({\n          strings: lcObj\n        }).where('code', localeCode)\n      } else {\n        await WIKI.models.locales.query().insert({\n          code: localeCode,\n          strings: lcObj,\n          isRTL: currentLocale.isRTL,\n          name: currentLocale.name,\n          nativeName: currentLocale.nativeName,\n          availability: currentLocale.availability\n        })\n      }\n    } else {\n      throw new Error('Failed to fetch cached locales list! Restart server to resolve this issue.')\n    }\n\n    await WIKI.lang.refreshNamespaces()\n\n    WIKI.logger.info(`Fetching locale ${localeCode} from Graph endpoint: [ COMPLETED ]`)\n  } catch (err) {\n    WIKI.logger.error(`Fetching locale ${localeCode} from Graph endpoint: [ FAILED ]`)\n    WIKI.logger.error(err.message)\n  }\n}\n"
  },
  {
    "path": "server/jobs/purge-uploads.js",
    "content": "/* global WIKI */\n\nconst Promise = require('bluebird')\nconst fs = require('fs-extra')\nconst moment = require('moment')\nconst path = require('path')\n\nmodule.exports = async () => {\n  WIKI.logger.info('Purging orphaned upload files...')\n\n  try {\n    const uplTempPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads')\n    await fs.ensureDir(uplTempPath)\n    const ls = await fs.readdir(uplTempPath)\n    const fifteenAgo = moment().subtract(15, 'minutes')\n\n    await Promise.map(ls, (f) => {\n      return fs.stat(path.join(uplTempPath, f)).then((s) => { return { filename: f, stat: s } })\n    }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => {\n      return Promise.map(arrFiles, (f) => {\n        if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {\n          return fs.unlink(path.join(uplTempPath, f.filename))\n        }\n      })\n    })\n\n    WIKI.logger.info('Purging orphaned upload files: [ COMPLETED ]')\n  } catch (err) {\n    WIKI.logger.error('Purging orphaned upload files: [ FAILED ]')\n    WIKI.logger.error(err.message)\n  }\n}\n"
  },
  {
    "path": "server/jobs/rebuild-tree.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = async (pageId) => {\n  WIKI.logger.info(`Rebuilding page tree...`)\n\n  try {\n    WIKI.models = require('../core/db').init()\n    await WIKI.configSvc.loadFromDb()\n    await WIKI.configSvc.applyFlags()\n\n    const pages = await WIKI.models.pages.query().select('id', 'path', 'localeCode', 'title', 'isPrivate', 'privateNS').orderBy(['localeCode', 'path'])\n    let tree = []\n    let pik = 0\n\n    for (const page of pages) {\n      const pagePaths = page.path.split('/')\n      let currentPath = ''\n      let depth = 0\n      let parentId = null\n      let ancestors = []\n      for (const part of pagePaths) {\n        depth++\n        const isFolder = (depth < pagePaths.length)\n        currentPath = currentPath ? `${currentPath}/${part}` : part\n        const found = _.find(tree, {\n          localeCode: page.localeCode,\n          path: currentPath\n        })\n        if (!found) {\n          pik++\n          tree.push({\n            id: pik,\n            localeCode: page.localeCode,\n            path: currentPath,\n            depth: depth,\n            title: isFolder ? part : page.title,\n            isFolder: isFolder,\n            isPrivate: !isFolder && page.isPrivate,\n            privateNS: !isFolder ? page.privateNS : null,\n            parent: parentId,\n            pageId: isFolder ? null : page.id,\n            ancestors: JSON.stringify(ancestors)\n          })\n          parentId = pik\n        } else if (isFolder && !found.isFolder) {\n          found.isFolder = true\n          parentId = found.id\n        } else {\n          parentId = found.id\n        }\n        ancestors.push(parentId)\n      }\n    }\n\n    await WIKI.models.knex.table('pageTree').truncate()\n    if (tree.length > 0) {\n      // -> Save in chunks, because of per query max parameters (35k Postgres, 2k MSSQL, 1k for SQLite)\n      if ((WIKI.config.db.type !== 'sqlite')) {\n        for (const chunk of _.chunk(tree, 100)) {\n          await WIKI.models.knex.table('pageTree').insert(chunk)\n        }\n      } else {\n        for (const chunk of _.chunk(tree, 60)) {\n          await WIKI.models.knex.table('pageTree').insert(chunk)\n        }\n      }\n    }\n\n    await WIKI.models.knex.destroy()\n\n    WIKI.logger.info(`Rebuilding page tree: [ COMPLETED ]`)\n  } catch (err) {\n    WIKI.logger.error(`Rebuilding page tree: [ FAILED ]`)\n    WIKI.logger.error(err.message)\n    // exit process with error code\n    throw err\n  }\n}\n"
  },
  {
    "path": "server/jobs/render-page.js",
    "content": "const _ = require('lodash')\nconst cheerio = require('cheerio')\n\n/* global WIKI */\n\nmodule.exports = async (pageId) => {\n  WIKI.logger.info(`Rendering page ID ${pageId}...`)\n\n  try {\n    WIKI.models = require('../core/db').init()\n    await WIKI.configSvc.loadFromDb()\n    await WIKI.configSvc.applyFlags()\n\n    const page = await WIKI.models.pages.getPageFromDb(pageId)\n    if (!page) {\n      throw new Error('Invalid Page Id')\n    }\n\n    await WIKI.models.renderers.fetchDefinitions()\n    const pipeline = await WIKI.models.renderers.getRenderingPipeline(page.contentType)\n\n    let output = page.content\n\n    if (_.isEmpty(page.content)) {\n      await WIKI.models.knex.destroy()\n      WIKI.logger.warn(`Failed to render page ID ${pageId} because content was empty: [ FAILED ]`)\n    }\n\n    for (let core of pipeline) {\n      const renderer = require(`../modules/rendering/${_.kebabCase(core.key)}/renderer.js`)\n      output = await renderer.render.call({\n        config: core.config,\n        children: core.children,\n        page: page,\n        input: output\n      })\n    }\n\n    // Parse TOC\n    const $ = cheerio.load(output)\n    let isStrict = $('h1').length > 0 // <- Allows for documents using H2 as top level\n    let toc = { root: [] }\n\n    $('h1,h2,h3,h4,h5,h6').each((idx, el) => {\n      const depth = _.toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2)\n      let leafPathError = false\n\n      const leafPath = _.reduce(_.times(depth), (curPath, curIdx) => {\n        if (_.has(toc, curPath)) {\n          const lastLeafIdx = _.get(toc, curPath).length - 1\n          if (lastLeafIdx >= 0) {\n            curPath = `${curPath}[${lastLeafIdx}].children`\n          } else {\n            leafPathError = true\n          }\n        }\n        return curPath\n      }, 'root')\n\n      if (leafPathError) { return }\n\n      const leafSlug = $('.toc-anchor', el).first().attr('href')\n      $('.toc-anchor', el).remove()\n\n      _.get(toc, leafPath).push({\n        title: _.trim($(el).text()),\n        anchor: leafSlug,\n        children: []\n      })\n    })\n\n    // Save to DB\n    await WIKI.models.pages.query()\n      .patch({\n        render: output,\n        toc: JSON.stringify(toc.root)\n      })\n      .where('id', pageId)\n\n    // Save to cache\n    await WIKI.models.pages.savePageToCache({\n      ...page,\n      render: output,\n      toc: JSON.stringify(toc.root)\n    })\n\n    await WIKI.models.knex.destroy()\n\n    WIKI.logger.info(`Rendering page ID ${pageId}: [ COMPLETED ]`)\n  } catch (err) {\n    WIKI.logger.error(`Rendering page ID ${pageId}: [ FAILED ]`)\n    WIKI.logger.error(err.message)\n    // exit process with error code\n    throw err\n  }\n}\n"
  },
  {
    "path": "server/jobs/sanitize-svg.js",
    "content": "const fs = require('fs-extra')\nconst { JSDOM } = require('jsdom')\nconst createDOMPurify = require('dompurify')\n\n/* global WIKI */\n\nmodule.exports = async (svgPath) => {\n  WIKI.logger.info(`Sanitizing SVG file upload...`)\n\n  try {\n    let svgContents = await fs.readFile(svgPath, 'utf8')\n\n    const window = new JSDOM('').window\n    const DOMPurify = createDOMPurify(window)\n\n    svgContents = DOMPurify.sanitize(svgContents)\n\n    await fs.writeFile(svgPath, svgContents)\n    WIKI.logger.info(`Sanitized SVG file upload: [ COMPLETED ]`)\n  } catch (err) {\n    WIKI.logger.error(`Failed to sanitize SVG file upload: [ FAILED ]`)\n    WIKI.logger.error(err.message)\n    throw err\n  }\n}\n"
  },
  {
    "path": "server/jobs/sync-graph-locales.js",
    "content": "const _ = require('lodash')\nconst { createApolloFetch } = require('apollo-fetch')\n\n/* global WIKI */\n\nmodule.exports = async () => {\n  WIKI.logger.info('Syncing locales with Graph endpoint...')\n\n  try {\n    const apollo = createApolloFetch({\n      uri: WIKI.config.graphEndpoint\n    })\n\n    // -> Fetch locales list\n\n    const respList = await apollo({\n      query: `{\n        localization {\n          locales {\n            availability\n            code\n            name\n            nativeName\n            isRTL\n            createdAt\n            updatedAt\n          }\n        }\n      }`\n    })\n    const locales = _.sortBy(_.get(respList, 'data.localization.locales', []), 'name').map(lc => ({...lc, isInstalled: (lc.code === 'en')}))\n    WIKI.cache.set('locales', locales)\n\n    // -> Download locale strings\n\n    if (WIKI.config.lang.autoUpdate) {\n      const activeLocales = WIKI.config.lang.namespacing ? WIKI.config.lang.namespaces : [WIKI.config.lang.code]\n      for (const currentLocale of activeLocales) {\n        const localeInfo = _.find(locales, ['code', currentLocale])\n\n        const respStrings = await apollo({\n          query: `query ($code: String!) {\n            localization {\n              strings(code: $code) {\n                key\n                value\n              }\n            }\n          }`,\n          variables: {\n            code: currentLocale\n          }\n        })\n        const strings = _.get(respStrings, 'data.localization.strings', [])\n        let lcObj = {}\n        _.forEach(strings, row => {\n          if (_.includes(row.key, '::')) { return }\n          if (_.isEmpty(row.value)) {\n            row.value = row.key\n          }\n          _.set(lcObj, row.key.replace(':', '.'), row.value)\n        })\n\n        await WIKI.models.locales.query().update({\n          code: currentLocale,\n          strings: lcObj,\n          isRTL: localeInfo.isRTL,\n          name: localeInfo.name,\n          nativeName: localeInfo.nativeName,\n          availability: localeInfo.availability\n        }).where('code', currentLocale)\n\n        WIKI.logger.info(`Pulled latest locale updates for ${localeInfo.name} from Graph endpoint: [ COMPLETED ]`)\n      }\n    }\n\n    await WIKI.lang.refreshNamespaces()\n\n    WIKI.logger.info('Syncing locales with Graph endpoint: [ COMPLETED ]')\n  } catch (err) {\n    WIKI.logger.error('Syncing locales with Graph endpoint: [ FAILED ]')\n    WIKI.logger.error(err.message)\n  }\n}\n"
  },
  {
    "path": "server/jobs/sync-graph-updates.js",
    "content": "const _ = require('lodash')\nconst { createApolloFetch } = require('apollo-fetch')\n\n/* global WIKI */\n\nmodule.exports = async () => {\n  WIKI.logger.info(`Fetching latest updates from Graph endpoint...`)\n\n  try {\n    const apollo = createApolloFetch({\n      uri: WIKI.config.graphEndpoint\n    })\n\n    const resp = await apollo({\n      query: `query ($channel: ReleaseChannel!, $version: String!) {\n        releases {\n          checkForUpdates(channel: $channel, version: $version) {\n            channel\n            version\n            releaseDate\n            minimumVersionRequired\n            minimumNodeRequired\n          }\n        }\n      }`,\n      variables: {\n        channel: WIKI.config.channel,\n        version: WIKI.version\n      }\n    })\n    const info = _.get(resp, 'data.releases.checkForUpdates', false)\n    if (info) {\n      WIKI.system.updates = info\n    }\n\n    WIKI.logger.info(`Fetching latest updates from Graph endpoint: [ COMPLETED ]`)\n  } catch (err) {\n    WIKI.logger.error(`Fetching latest updates from Graph endpoint: [ FAILED ]`)\n    WIKI.logger.error(err.message)\n  }\n}\n"
  },
  {
    "path": "server/jobs/sync-storage.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = async (targetKey) => {\n  WIKI.logger.info(`Syncing with storage target ${targetKey}...`)\n\n  try {\n    const target = _.find(WIKI.models.storage.targets, ['key', targetKey])\n    if (target) {\n      await target.fn.sync()\n      WIKI.logger.info(`Syncing with storage target ${targetKey}: [ COMPLETED ]`)\n\n      await WIKI.models.storage.query().patch({\n        state: {\n          status: 'operational',\n          message: '',\n          lastAttempt: new Date().toISOString()\n        }\n      }).where('key', targetKey)\n    } else {\n      throw new Error('Invalid storage target. Unable to perform sync.')\n    }\n  } catch (err) {\n    WIKI.logger.error(`Syncing with storage target ${targetKey}: [ FAILED ]`)\n    WIKI.logger.error(err.message)\n    await WIKI.models.storage.query().patch({\n      state: {\n        status: 'error',\n        message: err.message,\n        lastAttempt: new Date().toISOString()\n      }\n    }).where('key', targetKey)\n  }\n}\n"
  },
  {
    "path": "server/locales/README.md",
    "content": "## IMPORTANT\n\nLocalization files are not stored into files!\n\nContact us on Gitter to request access to the translation web service: https://gitter.im/Requarks/wiki\n\n## Development Mode\n\nIf 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.:\n\n### en.yml\n```yml\nadmin:\n  api.title: 'API Access'\n  auth.title: 'Authentication'\n```\n\nThe official localization keys will still be loaded first, but your local files will overwrite any existing keys (and add new ones).\n\nNote that you must restart Wiki.js to load any changes made to the files, which happens automatically on save when in dev mode.\n"
  },
  {
    "path": "server/master.js",
    "content": "const autoload = require('auto-load')\nconst bodyParser = require('body-parser')\nconst compression = require('compression')\nconst cookieParser = require('cookie-parser')\nconst cors = require('cors')\nconst express = require('express')\nconst session = require('express-session')\nconst KnexSessionStore = require('connect-session-knex')(session)\nconst favicon = require('serve-favicon')\nconst path = require('path')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nmodule.exports = async () => {\n  // ----------------------------------------\n  // Load core modules\n  // ----------------------------------------\n\n  WIKI.auth = require('./core/auth').init()\n  WIKI.lang = require('./core/localization').init()\n  WIKI.mail = require('./core/mail').init()\n  WIKI.system = require('./core/system').init()\n\n  // ----------------------------------------\n  // Load middlewares\n  // ----------------------------------------\n\n  const mw = autoload(path.join(WIKI.SERVERPATH, '/middlewares'))\n  const ctrl = autoload(path.join(WIKI.SERVERPATH, '/controllers'))\n\n  // ----------------------------------------\n  // Define Express App\n  // ----------------------------------------\n\n  const app = express()\n  WIKI.app = app\n  app.use(compression())\n\n  // ----------------------------------------\n  // Security\n  // ----------------------------------------\n\n  app.use(mw.security)\n  app.use(cors({ origin: false }))\n  app.options('*', cors({ origin: false }))\n  if (WIKI.config.security.securityTrustProxy) {\n    app.enable('trust proxy')\n  }\n\n  // ----------------------------------------\n  // Public Assets\n  // ----------------------------------------\n\n  app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))\n  app.use('/_assets/svg/twemoji', async (req, res, next) => {\n    try {\n      WIKI.asar.serve('twemoji', req, res, next)\n    } catch (err) {\n      res.sendStatus(404)\n    }\n  })\n  app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets'), {\n    index: false,\n    maxAge: '7d'\n  }))\n\n  // ----------------------------------------\n  // SSL Handlers\n  // ----------------------------------------\n\n  app.use('/', ctrl.ssl)\n\n  // ----------------------------------------\n  // Passport Authentication\n  // ----------------------------------------\n\n  app.use(cookieParser())\n  app.use(session({\n    secret: WIKI.config.sessionSecret,\n    resave: false,\n    saveUninitialized: false,\n    store: new KnexSessionStore({\n      knex: WIKI.models.knex\n    })\n  }))\n  app.use(WIKI.auth.passport.initialize())\n  app.use(WIKI.auth.authenticate)\n\n  // ----------------------------------------\n  // GraphQL Server\n  // ----------------------------------------\n\n  app.use(bodyParser.json({ limit: WIKI.config.bodyParserLimit || '1mb' }))\n  await WIKI.servers.startGraphQL()\n\n  // ----------------------------------------\n  // SEO\n  // ----------------------------------------\n\n  app.use(mw.seo)\n\n  // ----------------------------------------\n  // View Engine Setup\n  // ----------------------------------------\n\n  app.set('views', path.join(WIKI.SERVERPATH, 'views'))\n  app.set('view engine', 'pug')\n\n  app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }))\n\n  // ----------------------------------------\n  // Localization\n  // ----------------------------------------\n\n  WIKI.lang.attachMiddleware(app)\n\n  // ----------------------------------------\n  // View accessible data\n  // ----------------------------------------\n\n  app.locals.siteConfig = {}\n  app.locals.analyticsCode = {}\n  app.locals.basedir = WIKI.ROOTPATH\n  app.locals.config = WIKI.config\n  app.locals.pageMeta = {\n    title: '',\n    description: WIKI.config.description,\n    image: '',\n    url: '/'\n  }\n  app.locals.devMode = WIKI.devMode\n\n  // ----------------------------------------\n  // HMR (Dev Mode Only)\n  // ----------------------------------------\n\n  if (global.DEV) {\n    app.use(global.WP_DEV.devMiddleware)\n    app.use(global.WP_DEV.hotMiddleware)\n  }\n\n  // ----------------------------------------\n  // Routing\n  // ----------------------------------------\n\n  app.use(async (req, res, next) => {\n    res.locals.siteConfig = {\n      title: WIKI.config.title,\n      theme: WIKI.config.theming.theme,\n      darkMode: WIKI.config.theming.darkMode,\n      tocPosition: WIKI.config.theming.tocPosition || 'left',\n      lang: WIKI.config.lang.code,\n      rtl: WIKI.config.lang.rtl,\n      company: WIKI.config.company,\n      contentLicense: WIKI.config.contentLicense,\n      footerOverride: WIKI.config.footerOverride,\n      logoUrl: WIKI.config.logoUrl\n    }\n    res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true })\n    res.locals.analyticsCode = await WIKI.models.analytics.getCode({ cache: true })\n    next()\n  })\n\n  app.use('/', ctrl.auth)\n  app.use('/', ctrl.upload)\n  app.use('/', ctrl.common)\n\n  // ----------------------------------------\n  // Error handling\n  // ----------------------------------------\n\n  app.use((req, res, next) => {\n    const err = new Error('Not Found')\n    err.status = 404\n    next(err)\n  })\n\n  app.use((err, req, res, next) => {\n    if (req.path === '/graphql') {\n      res.status(err.status || 500).json({\n        data: {},\n        errors: [{\n          message: err.message,\n          path: []\n        }]\n      })\n    } else {\n      res.status(err.status || 500)\n      _.set(res.locals, 'pageMeta.title', 'Error')\n      res.render('error', {\n        message: err.message,\n        error: WIKI.IS_DEBUG ? err : {}\n      })\n    }\n  })\n\n  // ----------------------------------------\n  // Start HTTP Server(s)\n  // ----------------------------------------\n\n  await WIKI.servers.startHTTP()\n\n  if (WIKI.config.ssl.enabled === true || WIKI.config.ssl.enabled === 'true' || WIKI.config.ssl.enabled === 1 || WIKI.config.ssl.enabled === '1') {\n    await WIKI.servers.startHTTPS()\n  }\n\n  return true\n}\n"
  },
  {
    "path": "server/middlewares/security.js",
    "content": "/* global WIKI */\n\n/**\n * Security Middleware\n *\n * @param      {Express Request}   req     Express request object\n * @param      {Express Response}  res     Express response object\n * @param      {Function}          next    next callback function\n * @return     {any}               void\n */\nmodule.exports = function (req, res, next) {\n  // -> Disable X-Powered-By\n  req.app.disable('x-powered-by')\n\n  // -> Disable Frame Embedding\n  if (WIKI.config.security.securityIframe) {\n    res.set('X-Frame-Options', 'deny')\n  }\n\n  // -> Re-enable XSS Fitler if disabled\n  res.set('X-XSS-Protection', '1; mode=block')\n\n  // -> Disable MIME-sniffing\n  res.set('X-Content-Type-Options', 'nosniff')\n\n  // -> Disable IE Compatibility Mode\n  res.set('X-UA-Compatible', 'IE=edge')\n\n  // -> Disables referrer header when navigating to a different origin\n  if (WIKI.config.security.securityReferrerPolicy) {\n    res.set('Referrer-Policy', 'same-origin')\n  }\n\n  // -> Enforce HSTS\n  if (WIKI.config.security.securityHSTS) {\n    res.set('Strict-Transport-Security', `max-age=${WIKI.config.security.securityHSTSDuration}; includeSubDomains`)\n  }\n\n  // -> Prevent Open Redirect from user provided URL\n  if (WIKI.config.security.securityOpenRedirect) {\n    // Strips out all repeating / character in the provided URL\n    req.url = req.url.replace(/(\\/)(?=\\/*\\1)/g, '')\n  }\n\n  return next()\n}\n"
  },
  {
    "path": "server/middlewares/seo.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n/**\n * SEO Middleware\n *\n * @param      {Express Request}   req     Express request object\n * @param      {Express Response}  res     Express response object\n * @param      {Function}          next    next callback function\n * @return     {any}               void\n */\nmodule.exports = function (req, res, next) {\n  if (req.path.length > 1 && _.endsWith(req.path, '/')) {\n    let query = req.url.slice(req.path.length) || ''\n    res.redirect(301, req.path.slice(0, -1) + query)\n  } else {\n    _.set(res.locals, 'pageMeta.url', `${WIKI.config.host}${req.path}`)\n    return next()\n  }\n}\n"
  },
  {
    "path": "server/models/analytics.js",
    "content": "const Model = require('objection').Model\nconst fs = require('fs-extra')\nconst path = require('path')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * Analytics model\n */\nmodule.exports = class Analytics extends Model {\n  static get tableName() { return 'analytics' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config']\n  }\n\n  static async getProviders(isEnabled) {\n    const providers = await WIKI.models.analytics.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {})\n    return _.sortBy(providers, ['key'])\n  }\n\n  static async refreshProvidersFromDisk() {\n    let trx\n    try {\n      const dbProviders = await WIKI.models.analytics.query()\n\n      // -> Fetch definitions from disk\n      const analyticsDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/analytics'))\n      let diskProviders = []\n      for (let dir of analyticsDirs) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', dir, 'definition.yml'), 'utf8')\n        diskProviders.push(yaml.safeLoad(def))\n      }\n      WIKI.data.analytics = diskProviders.map(provider => ({\n        ...provider,\n        props: commonHelper.parseModuleProps(provider.props)\n      }))\n\n      let newProviders = []\n      for (let provider of WIKI.data.analytics) {\n        if (!_.some(dbProviders, ['key', provider.key])) {\n          newProviders.push({\n            key: provider.key,\n            isEnabled: false,\n            config: _.transform(provider.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {})\n          })\n        } else {\n          const providerConfig = _.get(_.find(dbProviders, ['key', provider.key]), 'config', {})\n          await WIKI.models.analytics.query().patch({\n            config: _.transform(provider.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, providerConfig)\n          }).where('key', provider.key)\n        }\n      }\n      if (newProviders.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let provider of newProviders) {\n          await WIKI.models.analytics.query(trx).insert(provider)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newProviders.length} new analytics providers: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new analytics providers found: [ SKIPPED ]`)\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new analytics providers: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  static async getCode ({ cache = false } = {}) {\n    if (cache) {\n      const analyticsCached = await WIKI.cache.get('analytics')\n      if (analyticsCached) {\n        return analyticsCached\n      }\n    }\n    try {\n      const analyticsCode = {\n        head: '',\n        bodyStart: '',\n        bodyEnd: ''\n      }\n      const providers = await WIKI.models.analytics.getProviders(true)\n\n      for (let provider of providers) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', provider.key, 'code.yml'), 'utf8')\n        let code = yaml.safeLoad(def)\n        code.head = _.defaultTo(code.head, '')\n        code.bodyStart = _.defaultTo(code.bodyStart, '')\n        code.bodyEnd = _.defaultTo(code.bodyEnd, '')\n\n        _.forOwn(provider.config, (value, key) => {\n          code.head = _.replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)\n          code.bodyStart = _.replace(code.bodyStart, `{{${key}}}`, value)\n          code.bodyEnd = _.replace(code.bodyEnd, `{{${key}}}`, value)\n        })\n\n        analyticsCode.head += code.head\n        analyticsCode.bodyStart += code.bodyStart\n        analyticsCode.bodyEnd += code.bodyEnd\n      }\n\n      await WIKI.cache.set('analytics', analyticsCode, 300)\n\n      return analyticsCode\n    } catch (err) {\n      WIKI.logger.warn('Error while getting analytics code: ', err)\n      return {\n        head: '',\n        bodyStart: '',\n        bodyEnd: ''\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/apiKeys.js",
    "content": "/* global WIKI */\n\nconst Model = require('objection').Model\nconst moment = require('moment')\nconst ms = require('ms')\nconst jwt = require('jsonwebtoken')\n\n/**\n * Users model\n */\nmodule.exports = class ApiKey extends Model {\n  static get tableName() { return 'apiKeys' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['name', 'key'],\n\n      properties: {\n        id: {type: 'integer'},\n        name: {type: 'string'},\n        key: {type: 'string'},\n        expiration: {type: 'string'},\n        isRevoked: {type: 'boolean'},\n        createdAt: {type: 'string'},\n        validUntil: {type: 'string'}\n      }\n    }\n  }\n\n  async $beforeUpdate(opt, context) {\n    await super.$beforeUpdate(opt, context)\n\n    this.updatedAt = moment.utc().toISOString()\n  }\n  async $beforeInsert(context) {\n    await super.$beforeInsert(context)\n\n    this.createdAt = moment.utc().toISOString()\n    this.updatedAt = moment.utc().toISOString()\n  }\n\n  static async createNewKey ({ name, expiration, fullAccess, group }) {\n    const entry = await WIKI.models.apiKeys.query().insert({\n      name,\n      key: 'pending',\n      expiration: moment.utc().add(ms(expiration), 'ms').toISOString(),\n      isRevoked: true\n    })\n\n    const key = jwt.sign({\n      api: entry.id,\n      grp: fullAccess ? 1 : group\n    }, {\n      key: WIKI.config.certs.private,\n      passphrase: WIKI.config.sessionSecret\n    }, {\n      algorithm: 'RS256',\n      expiresIn: expiration,\n      audience: WIKI.config.auth.audience,\n      issuer: 'urn:wiki.js'\n    })\n\n    await WIKI.models.apiKeys.query().findById(entry.id).patch({\n      key,\n      isRevoked: false\n    })\n\n    return key\n  }\n}\n"
  },
  {
    "path": "server/models/assetFolders.js",
    "content": "const Model = require('objection').Model\nconst _ = require('lodash')\n\n/* global WIKI */\n\n/**\n * Users model\n */\nmodule.exports = class AssetFolder extends Model {\n  static get tableName() { return 'assetFolders' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n\n      properties: {\n        id: {type: 'integer'},\n        name: {type: 'string'},\n        slug: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      parent: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: AssetFolder,\n        join: {\n          from: 'assetFolders.folderId',\n          to: 'assetFolders.id'\n        }\n      }\n    }\n  }\n\n  /**\n   * Get full folder hierarchy starting from specified folder to root\n   *\n   * @param {Number} folderId Id of the folder\n   */\n  static async getHierarchy (folderId) {\n    let hier\n    if (WIKI.config.db.type === 'mssql') {\n      hier = await WIKI.models.knex.with('ancestors', qb => {\n        qb.select('id', 'name', 'slug', 'parentId').from('assetFolders').where('id', folderId).unionAll(sqb => {\n          sqb.select('a.id', 'a.name', 'a.slug', 'a.parentId').from('assetFolders AS a').join('ancestors', 'ancestors.parentId', 'a.id')\n        })\n      }).select('*').from('ancestors')\n    } else {\n      hier = await WIKI.models.knex.withRecursive('ancestors', qb => {\n        qb.select('id', 'name', 'slug', 'parentId').from('assetFolders').where('id', folderId).union(sqb => {\n          sqb.select('a.id', 'a.name', 'a.slug', 'a.parentId').from('assetFolders AS a').join('ancestors', 'ancestors.parentId', 'a.id')\n        })\n      }).select('*').from('ancestors')\n    }\n    // The ancestors are from children to grandparents, must reverse for correct path order.\n    return _.reverse(hier)\n  }\n\n  /**\n   * Get full folder paths\n   */\n  static async getAllPaths () {\n    const all = await WIKI.models.assetFolders.query()\n    let folders = {}\n    all.forEach(fld => {\n      _.set(folders, fld.id, fld.slug)\n      let parentId = fld.parentId\n      while (parentId !== null || parentId > 0) {\n        const parent = _.find(all, ['id', parentId])\n        _.set(folders, fld.id, `${parent.slug}/${_.get(folders, fld.id)}`)\n        parentId = parent.parentId\n      }\n    })\n    return folders\n  }\n}\n"
  },
  {
    "path": "server/models/assets.js",
    "content": "/* global WIKI */\n\nconst Model = require('objection').Model\nconst moment = require('moment')\nconst path = require('path')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst assetHelper = require('../helpers/asset')\nconst Promise = require('bluebird')\n\n/**\n * Users model\n */\nmodule.exports = class Asset extends Model {\n  static get tableName() { return 'assets' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n\n      properties: {\n        id: {type: 'integer'},\n        filename: {type: 'string'},\n        hash: {type: 'string'},\n        ext: {type: 'string'},\n        kind: {type: 'string'},\n        mime: {type: 'string'},\n        fileSize: {type: 'integer'},\n        metadata: {type: 'object'},\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      author: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'assets.authorId',\n          to: 'users.id'\n        }\n      },\n      folder: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./assetFolders'),\n        join: {\n          from: 'assets.folderId',\n          to: 'assetFolders.id'\n        }\n      }\n    }\n  }\n\n  async $beforeUpdate(opt, context) {\n    await super.$beforeUpdate(opt, context)\n\n    this.updatedAt = moment.utc().toISOString()\n  }\n  async $beforeInsert(context) {\n    await super.$beforeInsert(context)\n\n    this.createdAt = moment.utc().toISOString()\n    this.updatedAt = moment.utc().toISOString()\n  }\n\n  async getAssetPath() {\n    let hierarchy = []\n    if (this.folderId) {\n      hierarchy = await WIKI.models.assetFolders.getHierarchy(this.folderId)\n    }\n    return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename\n  }\n\n  async deleteAssetCache() {\n    await fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))\n  }\n\n  static async upload(opts) {\n    const fileInfo = path.parse(opts.originalname)\n    const fileHash = assetHelper.generateHash(opts.assetPath)\n\n    // Check for existing asset\n    let asset = await WIKI.models.assets.query().where({\n      hash: fileHash,\n      folderId: opts.folderId\n    }).first()\n\n    // Build Object\n    let assetRow = {\n      filename: opts.originalname,\n      hash: fileHash,\n      ext: fileInfo.ext,\n      kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',\n      mime: opts.mimetype,\n      fileSize: opts.size,\n      folderId: opts.folderId\n    }\n\n    // Sanitize SVG contents\n    if (\n      WIKI.config.uploads.scanSVG &&\n      (\n        opts.mimetype.toLowerCase().startsWith('image/svg') ||\n        fileInfo.ext.toLowerCase() === '.svg'\n      )\n    ) {\n      const svgSanitizeJob = await WIKI.scheduler.registerJob({\n        name: 'sanitize-svg',\n        immediate: true,\n        worker: true\n      }, opts.path)\n      await svgSanitizeJob.finished\n    }\n\n    // Save asset data\n    try {\n      const fileBuffer = await fs.readFile(opts.path)\n\n      if (asset) {\n        // Patch existing asset\n        if (opts.mode === 'upload') {\n          assetRow.authorId = opts.user.id\n        }\n        await WIKI.models.assets.query().patch(assetRow).findById(asset.id)\n        await WIKI.models.knex('assetData').where({\n          id: asset.id\n        }).update({\n          data: fileBuffer\n        })\n      } else {\n        // Create asset entry\n        assetRow.authorId = opts.user.id\n        asset = await WIKI.models.assets.query().insert(assetRow)\n        await WIKI.models.knex('assetData').insert({\n          id: asset.id,\n          data: fileBuffer\n        })\n      }\n\n      // Move temp upload to cache\n      if (opts.mode === 'upload') {\n        await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })\n      } else {\n        await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })\n      }\n\n      // Add to Storage\n      if (!opts.skipStorage) {\n        await WIKI.models.storage.assetEvent({\n          event: 'uploaded',\n          asset: {\n            ...asset,\n            path: await asset.getAssetPath(),\n            data: fileBuffer,\n            authorId: opts.user.id,\n            authorName: opts.user.name,\n            authorEmail: opts.user.email\n          }\n        })\n      }\n    } catch (err) {\n      WIKI.logger.warn(err)\n    }\n  }\n\n  static async getAsset(assetPath, res) {\n    try {\n      const fileInfo = assetHelper.getPathInfo(assetPath)\n      const fileHash = assetHelper.generateHash(assetPath)\n      const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`)\n\n      // Force unsafe extensions to download\n      if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {\n        res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))\n      }\n\n      if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) {\n        return\n      }\n      if (await WIKI.models.assets.getAssetFromStorage(assetPath, res)) {\n        return\n      }\n      await WIKI.models.assets.getAssetFromDb(assetPath, fileHash, cachePath, res)\n    } catch (err) {\n      if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {\n        return\n      }\n      WIKI.logger.error(err)\n      res.sendStatus(500)\n    }\n  }\n\n  static async getAssetFromCache(assetPath, cachePath, res) {\n    try {\n      await fs.access(cachePath, fs.constants.R_OK)\n    } catch (err) {\n      return false\n    }\n    const sendFile = Promise.promisify(res.sendFile, {context: res})\n    res.type(path.extname(assetPath))\n    await sendFile(cachePath, { dotfiles: 'deny' })\n    return true\n  }\n\n  static async getAssetFromStorage(assetPath, res) {\n    const localLocations = await WIKI.models.storage.getLocalLocations({\n      asset: {\n        path: assetPath\n      }\n    })\n    for (let location of _.filter(localLocations, location => Boolean(location.path))) {\n      const assetExists = await WIKI.models.assets.getAssetFromCache(assetPath, location.path, res)\n      if (assetExists) {\n        return true\n      }\n    }\n    return false\n  }\n\n  static async getAssetFromDb(assetPath, fileHash, cachePath, res) {\n    const asset = await WIKI.models.assets.query().where('hash', fileHash).first()\n    if (asset) {\n      const assetData = await WIKI.models.knex('assetData').where('id', asset.id).first()\n      res.type(asset.ext)\n      res.send(assetData.data)\n      await fs.outputFile(cachePath, assetData.data)\n    } else {\n      res.sendStatus(404)\n    }\n  }\n\n  static async flushTempUploads() {\n    return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))\n  }\n}\n"
  },
  {
    "path": "server/models/authentication.js",
    "content": "const Model = require('objection').Model\nconst fs = require('fs-extra')\nconst path = require('path')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * Authentication model\n */\nmodule.exports = class Authentication extends Model {\n  static get tableName() { return 'authentication' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key'],\n\n      properties: {\n        key: {type: 'string'},\n        selfRegistration: {type: 'boolean'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config', 'domainWhitelist', 'autoEnrollGroups']\n  }\n\n  static async getStrategy(key) {\n    return WIKI.models.authentication.query().findOne({ key })\n  }\n\n  static async getStrategies() {\n    const strategies = await WIKI.models.authentication.query().orderBy('order')\n    return strategies.map(str => ({\n      ...str,\n      domainWhitelist: _.get(str.domainWhitelist, 'v', []),\n      autoEnrollGroups: _.get(str.autoEnrollGroups, 'v', [])\n    }))\n  }\n\n  static async getStrategiesForLegacyClient() {\n    const strategies = await WIKI.models.authentication.query().select('key', 'selfRegistration')\n    let formStrategies = []\n    let socialStrategies = []\n\n    for (let stg of strategies) {\n      const stgInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {}\n      if (stgInfo.useForm) {\n        formStrategies.push({\n          key: stg.key,\n          title: stgInfo.title\n        })\n      } else {\n        socialStrategies.push({\n          ...stgInfo,\n          ...stg,\n          icon: await fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${stg.key}.svg`), 'utf8').catch(err => {\n            if (err.code === 'ENOENT') {\n              return null\n            }\n            throw err\n          })\n        })\n      }\n    }\n\n    return {\n      formStrategies,\n      socialStrategies\n    }\n  }\n\n  static async refreshStrategiesFromDisk() {\n    try {\n      const dbStrategies = await WIKI.models.authentication.query()\n\n      // -> Fetch definitions from disk\n      const authDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/authentication'))\n      WIKI.data.authentication = []\n      for (let dir of authDirs) {\n        const defRaw = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/authentication', dir, 'definition.yml'), 'utf8')\n        const def = yaml.safeLoad(defRaw)\n        WIKI.data.authentication.push({\n          ...def,\n          props: commonHelper.parseModuleProps(def.props)\n        })\n      }\n\n      for (const strategy of dbStrategies) {\n        let newProps = false\n        const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey])\n        if (!strategyDef) {\n          await WIKI.models.authentication.query().delete().where('key', strategy.key)\n          WIKI.logger.info(`Authentication strategy ${strategy.strategyKey} was removed from disk: [ REMOVED ]`)\n          continue\n        }\n        strategy.config = _.transform(strategyDef.props, (result, value, key) => {\n          if (!_.has(result, key)) {\n            _.set(result, key, value.default)\n            // we have some new properties added to an existing auth strategy to write to the database\n            newProps = true\n          }\n          return result\n        }, strategy.config)\n\n        // Fix pre-2.5 strategies displayName\n        if (!strategy.displayName) {\n          await WIKI.models.authentication.query().patch({\n            displayName: strategyDef.title\n          }).where('key', strategy.key)\n        }\n        // write existing auth model to database with new properties and defaults\n        if (newProps) {\n          await WIKI.models.authentication.query().patch({\n            config: strategy.config\n          }).where('key', strategy.key)\n        }\n      }\n\n      WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication strategies: [ OK ]`)\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new authentication providers: [ FAILED ]`)\n      WIKI.logger.error(err)\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/commentProviders.js",
    "content": "const Model = require('objection').Model\nconst fs = require('fs-extra')\nconst path = require('path')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * CommentProvider model\n */\nmodule.exports = class CommentProvider extends Model {\n  static get tableName() { return 'commentProviders' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config']\n  }\n\n  static async getProvider(key) {\n    return WIKI.models.commentProviders.query().findOne({ key })\n  }\n\n  static async getProviders(isEnabled) {\n    const providers = await WIKI.models.commentProviders.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {})\n    return _.sortBy(providers, ['key'])\n  }\n\n  static async refreshProvidersFromDisk() {\n    let trx\n    try {\n      const dbProviders = await WIKI.models.commentProviders.query()\n\n      // -> Fetch definitions from disk\n      const commentDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/comments'))\n      let diskProviders = []\n      for (let dir of commentDirs) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', dir, 'definition.yml'), 'utf8')\n        diskProviders.push(yaml.safeLoad(def))\n      }\n      WIKI.data.commentProviders = diskProviders.map(provider => ({\n        ...provider,\n        props: commonHelper.parseModuleProps(provider.props)\n      }))\n\n      let newProviders = []\n      for (let provider of WIKI.data.commentProviders) {\n        if (!_.some(dbProviders, ['key', provider.key])) {\n          newProviders.push({\n            key: provider.key,\n            isEnabled: provider.key === 'default',\n            config: _.transform(provider.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {})\n          })\n        } else {\n          const providerConfig = _.get(_.find(dbProviders, ['key', provider.key]), 'config', {})\n          await WIKI.models.commentProviders.query().patch({\n            config: _.transform(provider.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, providerConfig)\n          }).where('key', provider.key)\n        }\n      }\n      if (newProviders.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let provider of newProviders) {\n          await WIKI.models.commentProviders.query(trx).insert(provider)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newProviders.length} new comment providers: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new comment providers found: [ SKIPPED ]`)\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new comment providers: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  static async initProvider() {\n    const commentProvider = await WIKI.models.commentProviders.query().findOne('isEnabled', true)\n    if (commentProvider) {\n      WIKI.data.commentProvider = {\n        ..._.find(WIKI.data.commentProviders, ['key', commentProvider.key]),\n        head: '',\n        bodyStart: '',\n        bodyEnd: '',\n        main: '<comments></comments>'\n      }\n\n      if (WIKI.data.commentProvider.codeTemplate) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', commentProvider.key, 'code.yml'), 'utf8')\n        let code = yaml.safeLoad(def)\n        code.head = _.defaultTo(code.head, '')\n        code.body = _.defaultTo(code.body, '')\n        code.main = _.defaultTo(code.main, '')\n\n        _.forOwn(commentProvider.config, (value, key) => {\n          code.head = _.replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)\n          code.body = _.replace(code.body, new RegExp(`{{${key}}}`, 'g'), value)\n          code.main = _.replace(code.main, new RegExp(`{{${key}}}`, 'g'), value)\n        })\n\n        WIKI.data.commentProvider.head = code.head\n        WIKI.data.commentProvider.body = code.body\n        WIKI.data.commentProvider.main = code.main\n      } else {\n        WIKI.data.commentProvider = {\n          ...WIKI.data.commentProvider,\n          ...require(`../modules/comments/${commentProvider.key}/comment`),\n          config: commentProvider.config\n        }\n        await WIKI.data.commentProvider.init()\n      }\n      WIKI.data.commentProvider.config = commentProvider.config\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/comments.js",
    "content": "const Model = require('objection').Model\nconst validate = require('validate.js')\nconst _ = require('lodash')\n\n/* global WIKI */\n\n/**\n * Comments model\n */\nmodule.exports = class Comment extends Model {\n  static get tableName() { return 'comments' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: [],\n\n      properties: {\n        id: {type: 'integer'},\n        content: {type: 'string'},\n        render: {type: 'string'},\n        name: {type: 'string'},\n        email: {type: 'string'},\n        ip: {type: 'string'},\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      author: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'comments.authorId',\n          to: 'users.id'\n        }\n      },\n      page: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./pages'),\n        join: {\n          from: 'comments.pageId',\n          to: 'pages.id'\n        }\n      }\n    }\n  }\n\n  $beforeUpdate() {\n    this.updatedAt = new Date().toISOString()\n  }\n  $beforeInsert() {\n    this.createdAt = new Date().toISOString()\n    this.updatedAt = new Date().toISOString()\n  }\n\n  /**\n   * Post New Comment\n   */\n  static async postNewComment ({ pageId, replyTo, content, guestName, guestEmail, user, ip }) {\n    // -> Input validation\n    if (user.id === 2) {\n      const validation = validate({\n        email: _.toLower(guestEmail),\n        name: guestName\n      }, {\n        email: {\n          email: true,\n          length: {\n            maximum: 255\n          }\n        },\n        name: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255\n          }\n        }\n      }, { format: 'flat' })\n\n      if (validation && validation.length > 0) {\n        throw new WIKI.Error.InputInvalid(validation[0])\n      }\n    }\n\n    content = _.trim(content)\n    if (content.length < 2) {\n      throw new WIKI.Error.CommentContentMissing()\n    }\n\n    // -> Load Page\n    const page = await WIKI.models.pages.getPageFromDb(pageId)\n    if (page) {\n      if (!WIKI.auth.checkAccess(user, ['write:comments'], {\n        path: page.path,\n        locale: page.localeCode,\n        tags: page.tags\n      })) {\n        throw new WIKI.Error.CommentPostForbidden()\n      }\n    } else {\n      throw new WIKI.Error.PageNotFound()\n    }\n\n    // -> Process by comment provider\n    return WIKI.data.commentProvider.create({\n      page,\n      replyTo,\n      content,\n      user: {\n        ...user,\n        ...(user.id === 2) ? {\n          name: guestName,\n          email: guestEmail\n        } : {},\n        ip\n      }\n    })\n  }\n\n  /**\n   * Update an Existing Comment\n   */\n  static async updateComment ({ id, content, user, ip }) {\n    // -> Load Page\n    const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id)\n    if (!pageId) {\n      throw new WIKI.Error.CommentNotFound()\n    }\n    const page = await WIKI.models.pages.getPageFromDb(pageId)\n    if (page) {\n      if (!WIKI.auth.checkAccess(user, ['manage:comments'], {\n        path: page.path,\n        locale: page.localeCode,\n        tags: page.tags\n      })) {\n        throw new WIKI.Error.CommentManageForbidden()\n      }\n    } else {\n      throw new WIKI.Error.PageNotFound()\n    }\n\n    // -> Process by comment provider\n    return WIKI.data.commentProvider.update({\n      id,\n      content,\n      page,\n      user: {\n        ...user,\n        ip\n      }\n    })\n  }\n\n  /**\n   * Delete an Existing Comment\n   */\n  static async deleteComment ({ id, user, ip }) {\n    // -> Load Page\n    const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id)\n    if (!pageId) {\n      throw new WIKI.Error.CommentNotFound()\n    }\n    const page = await WIKI.models.pages.getPageFromDb(pageId)\n    if (page) {\n      if (!WIKI.auth.checkAccess(user, ['manage:comments'], {\n        path: page.path,\n        locale: page.localeCode,\n        tags: page.tags\n      })) {\n        throw new WIKI.Error.CommentManageForbidden()\n      }\n    } else {\n      throw new WIKI.Error.PageNotFound()\n    }\n\n    // -> Process by comment provider\n    await WIKI.data.commentProvider.remove({\n      id,\n      page,\n      user: {\n        ...user,\n        ip\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "server/models/editors.js",
    "content": "const Model = require('objection').Model\nconst fs = require('fs-extra')\nconst path = require('path')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * Editor model\n */\nmodule.exports = class Editor extends Model {\n  static get tableName() { return 'editors' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config']\n  }\n\n  static async getEditors() {\n    return WIKI.models.editors.query()\n  }\n\n  static async refreshEditorsFromDisk() {\n    let trx\n    try {\n      const dbEditors = await WIKI.models.editors.query()\n\n      // -> Fetch definitions from disk\n      const editorDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/editor'))\n      let diskEditors = []\n      for (let dir of editorDirs) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/editor', dir, 'definition.yml'), 'utf8')\n        diskEditors.push(yaml.safeLoad(def))\n      }\n      WIKI.data.editors = diskEditors.map(editor => ({\n        ...editor,\n        props: commonHelper.parseModuleProps(editor.props)\n      }))\n\n      // -> Insert new editors\n      let newEditors = []\n      for (let editor of WIKI.data.editors) {\n        if (!_.some(dbEditors, ['key', editor.key])) {\n          newEditors.push({\n            key: editor.key,\n            isEnabled: false,\n            config: _.transform(editor.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {})\n          })\n        } else {\n          const editorConfig = _.get(_.find(dbEditors, ['key', editor.key]), 'config', {})\n          await WIKI.models.editors.query().patch({\n            config: _.transform(editor.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, editorConfig)\n          }).where('key', editor.key)\n        }\n      }\n      if (newEditors.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let editor of newEditors) {\n          await WIKI.models.editors.query(trx).insert(editor)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newEditors.length} new editors: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new editors found: [ SKIPPED ]`)\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new editors: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  static async getDefaultEditor(contentType) {\n    // TODO - hardcoded for now\n    switch (contentType) {\n      case 'markdown':\n        return 'markdown'\n      case 'html':\n        return 'ckeditor'\n      case 'asciidoc':\n        return 'asciidoc'\n      default:\n        return 'code'\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/groups.js",
    "content": "const Model = require('objection').Model\n\n/**\n * Groups model\n */\nmodule.exports = class Group extends Model {\n  static get tableName() { return 'groups' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['name'],\n\n      properties: {\n        id: {type: 'integer'},\n        name: {type: 'string'},\n        isSystem: {type: 'boolean'},\n        redirectOnLogin: {type: 'string'},\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['permissions', 'pageRules']\n  }\n\n  static get relationMappings() {\n    return {\n      users: {\n        relation: Model.ManyToManyRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'groups.id',\n          through: {\n            from: 'userGroups.groupId',\n            to: 'userGroups.userId'\n          },\n          to: 'users.id'\n        }\n      }\n    }\n  }\n\n  $beforeUpdate() {\n    this.updatedAt = new Date().toISOString()\n  }\n  $beforeInsert() {\n    this.createdAt = new Date().toISOString()\n    this.updatedAt = new Date().toISOString()\n  }\n}\n"
  },
  {
    "path": "server/models/locales.js",
    "content": "const Model = require('objection').Model\n\n/* global WIKI */\n\n/**\n * Locales model\n */\nmodule.exports = class Locale extends Model {\n  static get tableName() { return 'locales' }\n  static get idColumn() { return 'code' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['code', 'name'],\n\n      properties: {\n        code: {type: 'string'},\n        isRTL: {type: 'boolean', default: false},\n        name: {type: 'string'},\n        nativeName: {type: 'string'},\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'},\n        availability: {type: 'integer'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['strings']\n  }\n\n  $beforeUpdate() {\n    this.updatedAt = new Date().toISOString()\n  }\n  $beforeInsert() {\n    this.createdAt = new Date().toISOString()\n    this.updatedAt = new Date().toISOString()\n  }\n\n  static async getNavLocales({ cache = false } = {}) {\n    if (!WIKI.config.lang.namespacing) {\n      return []\n    }\n\n    if (cache) {\n      const navLocalesCached = await WIKI.cache.get('nav:locales')\n      if (navLocalesCached) {\n        return navLocalesCached\n      }\n    }\n    const navLocales = await WIKI.models.locales.query().select('code', 'nativeName AS name').whereIn('code', WIKI.config.lang.namespaces).orderBy('code')\n    if (navLocales) {\n      if (cache) {\n        await WIKI.cache.set('nav:locales', navLocales, 300)\n      }\n      return navLocales\n    } else {\n      WIKI.logger.warn('Site Locales for navigation are missing or corrupted.')\n      return []\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/loggers.js",
    "content": "const Model = require('objection').Model\nconst path = require('path')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * Logger model\n */\nmodule.exports = class Logger extends Model {\n  static get tableName() { return 'loggers' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'},\n        level: {type: 'string'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config']\n  }\n\n  static async getLoggers() {\n    return WIKI.models.loggers.query()\n  }\n\n  static async refreshLoggersFromDisk() {\n    let trx\n    try {\n      const dbLoggers = await WIKI.models.loggers.query()\n\n      // -> Fetch definitions from disk\n      const loggersDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/logging'))\n      let diskLoggers = []\n      for (let dir of loggersDirs) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/logging', dir, 'definition.yml'), 'utf8')\n        diskLoggers.push(yaml.safeLoad(def))\n      }\n      WIKI.data.loggers = diskLoggers.map(logger => ({\n        ...logger,\n        props: commonHelper.parseModuleProps(logger.props)\n      }))\n\n      // -> Insert new loggers\n      let newLoggers = []\n      for (let logger of WIKI.data.loggers) {\n        if (!_.some(dbLoggers, ['key', logger.key])) {\n          newLoggers.push({\n            key: logger.key,\n            isEnabled: (logger.key === 'console'),\n            level: logger.defaultLevel,\n            config: _.transform(logger.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {})\n          })\n        } else {\n          const loggerConfig = _.get(_.find(dbLoggers, ['key', logger.key]), 'config', {})\n          await WIKI.models.loggers.query().patch({\n            config: _.transform(logger.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, loggerConfig)\n          }).where('key', logger.key)\n        }\n      }\n      if (newLoggers.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let logger of newLoggers) {\n          await WIKI.models.loggers.query(trx).insert(logger)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newLoggers.length} new loggers: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new loggers found: [ SKIPPED ]`)\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new loggers: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  static async pageEvent({ event, page }) {\n    const loggers = await WIKI.models.storage.query().where('isEnabled', true)\n    if (loggers && loggers.length > 0) {\n      _.forEach(loggers, logger => {\n        WIKI.queue.job.syncStorage.add({\n          event,\n          logger,\n          page\n        }, {\n          removeOnComplete: true\n        })\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/navigation.js",
    "content": "const Model = require('objection').Model\nconst _ = require('lodash')\n\n/* global WIKI */\n\n/**\n * Navigation model\n */\nmodule.exports = class Navigation extends Model {\n  static get tableName() { return 'navigation' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key'],\n\n      properties: {\n        key: {type: 'string'},\n        config: {type: 'array', items: {type: 'object'}}\n      }\n    }\n  }\n\n  static async getTree({ cache = false, locale = 'en', groups = [], bypassAuth = false } = {}) {\n    if (cache) {\n      const navTreeCached = await WIKI.cache.get(`nav:sidebar:${locale}`)\n      if (navTreeCached) {\n        return bypassAuth ? navTreeCached : WIKI.models.navigation.getAuthorizedItems(navTreeCached, groups)\n      }\n    }\n    const navTree = await WIKI.models.navigation.query().findOne('key', `site`)\n    if (navTree) {\n      // Check for pre-2.3 format\n      if (_.has(navTree.config[0], 'kind')) {\n        navTree.config = [{\n          locale: 'en',\n          items: navTree.config.map(item => ({\n            ...item,\n            visibilityMode: 'all',\n            visibilityGroups: []\n          }))\n        }]\n      }\n\n      for (const tree of navTree.config) {\n        if (cache) {\n          await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)\n        }\n      }\n      if (bypassAuth) {\n        return locale === 'all' ? navTree.config : WIKI.cache.get(`nav:sidebar:${locale}`)\n      } else {\n        return locale === 'all' ? WIKI.models.navigation.getAuthorizedItems(navTree.config, groups) : WIKI.models.navigation.getAuthorizedItems(WIKI.cache.get(`nav:sidebar:${locale}`), groups)\n      }\n    } else {\n      WIKI.logger.warn('Site Navigation is missing or corrupted.')\n      return []\n    }\n  }\n\n  static getAuthorizedItems(tree = [], groups = []) {\n    return _.filter(tree, leaf => {\n      return leaf.visibilityMode === 'all' || _.intersection(leaf.visibilityGroups, groups).length > 0\n    })\n  }\n}\n"
  },
  {
    "path": "server/models/pageHistory.js",
    "content": "const Model = require('objection').Model\nconst _ = require('lodash')\nconst { DateTime, Duration } = require('luxon')\n\n/* global WIKI */\n\n/**\n * Page History model\n */\nmodule.exports = class PageHistory extends Model {\n  static get tableName() { return 'pageHistory' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['path', 'title'],\n\n      properties: {\n        id: {type: 'integer'},\n        path: {type: 'string'},\n        hash: {type: 'string'},\n        title: {type: 'string'},\n        description: {type: 'string'},\n        isPublished: {type: 'boolean'},\n        publishStartDate: {type: 'string'},\n        publishEndDate: {type: 'string'},\n        content: {type: 'string'},\n        contentType: {type: 'string'},\n\n        createdAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      tags: {\n        relation: Model.ManyToManyRelation,\n        modelClass: require('./tags'),\n        join: {\n          from: 'pageHistory.id',\n          through: {\n            from: 'pageHistoryTags.pageId',\n            to: 'pageHistoryTags.tagId'\n          },\n          to: 'tags.id'\n        }\n      },\n      page: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./pages'),\n        join: {\n          from: 'pageHistory.pageId',\n          to: 'pages.id'\n        }\n      },\n      author: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'pageHistory.authorId',\n          to: 'users.id'\n        }\n      },\n      editor: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./editors'),\n        join: {\n          from: 'pageHistory.editorKey',\n          to: 'editors.key'\n        }\n      },\n      locale: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./locales'),\n        join: {\n          from: 'pageHistory.localeCode',\n          to: 'locales.code'\n        }\n      }\n    }\n  }\n\n  $beforeInsert() {\n    this.createdAt = new Date().toISOString()\n  }\n\n  /**\n   * Create Page Version\n   */\n  static async addVersion(opts) {\n    await WIKI.models.pageHistory.query().insert({\n      pageId: opts.id,\n      authorId: opts.authorId,\n      content: opts.content,\n      contentType: opts.contentType,\n      description: opts.description,\n      editorKey: opts.editorKey,\n      hash: opts.hash,\n      isPrivate: (opts.isPrivate === true || opts.isPrivate === 1),\n      isPublished: (opts.isPublished === true || opts.isPublished === 1),\n      localeCode: opts.localeCode,\n      path: opts.path,\n      publishEndDate: opts.publishEndDate || '',\n      publishStartDate: opts.publishStartDate || '',\n      title: opts.title,\n      action: opts.action || 'updated',\n      versionDate: opts.versionDate\n    })\n  }\n\n  /**\n   * Get Page Version\n   */\n  static async getVersion({ pageId, versionId }) {\n    const version = await WIKI.models.pageHistory.query()\n      .column([\n        'pageHistory.path',\n        'pageHistory.title',\n        'pageHistory.description',\n        'pageHistory.isPrivate',\n        'pageHistory.isPublished',\n        'pageHistory.publishStartDate',\n        'pageHistory.publishEndDate',\n        'pageHistory.content',\n        'pageHistory.contentType',\n        'pageHistory.createdAt',\n        'pageHistory.action',\n        'pageHistory.authorId',\n        'pageHistory.pageId',\n        'pageHistory.versionDate',\n        {\n          versionId: 'pageHistory.id',\n          editor: 'pageHistory.editorKey',\n          locale: 'pageHistory.localeCode',\n          authorName: 'author.name'\n        }\n      ])\n      .joinRelated('author')\n      .where({\n        'pageHistory.id': versionId,\n        'pageHistory.pageId': pageId\n      }).first()\n    if (version) {\n      return {\n        ...version,\n        updatedAt: version.createdAt || null,\n        tags: []\n      }\n    } else {\n      return null\n    }\n  }\n\n  /**\n   * Get History Trail of a Page\n   */\n  static async getHistory({ pageId, offsetPage = 0, offsetSize = 100 }) {\n    const history = await WIKI.models.pageHistory.query()\n      .column([\n        'pageHistory.id',\n        'pageHistory.path',\n        'pageHistory.authorId',\n        'pageHistory.action',\n        'pageHistory.versionDate',\n        {\n          authorName: 'author.name'\n        }\n      ])\n      .joinRelated('author')\n      .where({\n        'pageHistory.pageId': pageId\n      })\n      .orderBy('pageHistory.versionDate', 'desc')\n      .page(offsetPage, offsetSize)\n\n    let prevPh = null\n    const upperLimit = (offsetPage + 1) * offsetSize\n\n    if (history.total >= upperLimit) {\n      prevPh = await WIKI.models.pageHistory.query()\n        .column([\n          'pageHistory.id',\n          'pageHistory.path',\n          'pageHistory.authorId',\n          'pageHistory.action',\n          'pageHistory.versionDate',\n          {\n            authorName: 'author.name'\n          }\n        ])\n        .joinRelated('author')\n        .where({\n          'pageHistory.pageId': pageId\n        })\n        .orderBy('pageHistory.versionDate', 'desc')\n        .offset((offsetPage + 1) * offsetSize)\n        .limit(1)\n        .first()\n    }\n\n    return {\n      trail: _.reduce(_.reverse(history.results), (res, ph) => {\n        let actionType = 'edit'\n        let valueBefore = null\n        let valueAfter = null\n\n        if (!prevPh && history.total < upperLimit) {\n          actionType = 'initial'\n        } else if (_.get(prevPh, 'path', '') !== ph.path) {\n          actionType = 'move'\n          valueBefore = _.get(prevPh, 'path', '')\n          valueAfter = ph.path\n        }\n\n        res.unshift({\n          versionId: ph.id,\n          authorId: ph.authorId,\n          authorName: ph.authorName,\n          actionType,\n          valueBefore,\n          valueAfter,\n          versionDate: ph.versionDate\n        })\n\n        prevPh = ph\n        return res\n      }, []),\n      total: history.total\n    }\n  }\n\n  /**\n   * Purge history older than X\n   *\n   * @param {String} olderThan ISO 8601 Duration\n   */\n  static async purge (olderThan) {\n    const dur = Duration.fromISO(olderThan)\n    const olderThanISO = DateTime.utc().minus(dur)\n    await WIKI.models.pageHistory.query().where('versionDate', '<', olderThanISO.toISO()).del()\n  }\n}\n"
  },
  {
    "path": "server/models/pageLinks.js",
    "content": "const Model = require('objection').Model\n\n/**\n * Users model\n */\nmodule.exports = class PageLink extends Model {\n  static get tableName() { return 'pageLinks' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['path', 'localeCode'],\n\n      properties: {\n        id: {type: 'integer'},\n        path: {type: 'string'},\n        localeCode: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      page: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./pages'),\n        join: {\n          from: 'pageLinks.pageId',\n          to: 'pages.id'\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/pages.js",
    "content": "const Model = require('objection').Model\nconst _ = require('lodash')\nconst JSBinType = require('js-binary').Type\nconst pageHelper = require('../helpers/page')\nconst path = require('path')\nconst fs = require('fs-extra')\nconst yaml = require('js-yaml')\nconst striptags = require('striptags')\nconst emojiRegex = require('emoji-regex')\nconst he = require('he')\nconst CleanCSS = require('clean-css')\nconst TurndownService = require('turndown')\nconst turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm\nconst cheerio = require('cheerio')\n\n/* global WIKI */\n\nconst frontmatterRegex = {\n  html: /^(<!-{2}(?:\\n|\\r)([\\w\\W]+?)(?:\\n|\\r)-{2}>)?(?:\\n|\\r)*([\\w\\W]*)*/,\n  legacy: /^(<!-- TITLE: ?([\\w\\W]+?) ?-{2}>)?(?:\\n|\\r)?(<!-- SUBTITLE: ?([\\w\\W]+?) ?-{2}>)?(?:\\n|\\r)*([\\w\\W]*)*/i,\n  markdown: /^(-{3}(?:\\n|\\r)([\\w\\W]+?)(?:\\n|\\r)-{3})?(?:\\n|\\r)*([\\w\\W]*)*/\n}\n\nconst punctuationRegex = /[!,:;/\\\\_+\\-=()&#@<>$~%^*[\\]{}\"'|]+|(\\.\\s)|(\\s\\.)/ig\n// const htmlEntitiesRegex = /(&#[0-9]{3};)|(&#x[a-zA-Z0-9]{2};)/ig\n\n/**\n * Pages model\n */\nmodule.exports = class Page extends Model {\n  static get tableName() { return 'pages' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['path', 'title'],\n\n      properties: {\n        id: {type: 'integer'},\n        path: {type: 'string'},\n        hash: {type: 'string'},\n        title: {type: 'string'},\n        description: {type: 'string'},\n        isPublished: {type: 'boolean'},\n        privateNS: {type: 'string'},\n        publishStartDate: {type: 'string'},\n        publishEndDate: {type: 'string'},\n        content: {type: 'string'},\n        contentType: {type: 'string'},\n\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['extra']\n  }\n\n  static get relationMappings() {\n    return {\n      tags: {\n        relation: Model.ManyToManyRelation,\n        modelClass: require('./tags'),\n        join: {\n          from: 'pages.id',\n          through: {\n            from: 'pageTags.pageId',\n            to: 'pageTags.tagId'\n          },\n          to: 'tags.id'\n        }\n      },\n      links: {\n        relation: Model.HasManyRelation,\n        modelClass: require('./pageLinks'),\n        join: {\n          from: 'pages.id',\n          to: 'pageLinks.pageId'\n        }\n      },\n      author: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'pages.authorId',\n          to: 'users.id'\n        }\n      },\n      creator: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'pages.creatorId',\n          to: 'users.id'\n        }\n      },\n      editor: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./editors'),\n        join: {\n          from: 'pages.editorKey',\n          to: 'editors.key'\n        }\n      },\n      locale: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./locales'),\n        join: {\n          from: 'pages.localeCode',\n          to: 'locales.code'\n        }\n      }\n    }\n  }\n\n  $beforeUpdate() {\n    this.updatedAt = new Date().toISOString()\n  }\n  $beforeInsert() {\n    this.createdAt = new Date().toISOString()\n    this.updatedAt = new Date().toISOString()\n  }\n  /**\n   * Solving the violates foreign key constraint using cascade strategy\n   * using static hooks\n   * @see https://vincit.github.io/objection.js/api/types/#type-statichookarguments\n   */\n  static async beforeDelete({ asFindQuery }) {\n    const page = await asFindQuery().select('id')\n    await WIKI.models.comments.query().delete().where('pageId', page[0].id)\n  }\n  /**\n   * Cache Schema\n   */\n  static get cacheSchema() {\n    return new JSBinType({\n      id: 'uint',\n      authorId: 'uint',\n      authorName: 'string',\n      createdAt: 'string',\n      creatorId: 'uint',\n      creatorName: 'string',\n      description: 'string',\n      editorKey: 'string',\n      isPrivate: 'boolean',\n      isPublished: 'boolean',\n      publishEndDate: 'string',\n      publishStartDate: 'string',\n      contentType: 'string',\n      render: 'string',\n      tags: [\n        {\n          tag: 'string',\n          title: 'string'\n        }\n      ],\n      extra: {\n        js: 'string',\n        css: 'string'\n      },\n      title: 'string',\n      toc: 'string',\n      updatedAt: 'string'\n    })\n  }\n\n  /**\n   * Inject page metadata into contents\n   *\n   * @returns {string} Page Contents with Injected Metadata\n   */\n  injectMetadata () {\n    return pageHelper.injectPageMetadata(this)\n  }\n\n  /**\n   * Get the page's file extension based on content type\n   *\n   * @returns {string} File Extension\n   */\n  getFileExtension() {\n    return pageHelper.getFileExtension(this.contentType)\n  }\n\n  /**\n   * Parse injected page metadata from raw content\n   *\n   * @param {String} raw Raw file contents\n   * @param {String} contentType Content Type\n   * @returns {Object} Parsed Page Metadata with Raw Content\n   */\n  static parseMetadata (raw, contentType) {\n    let result\n    try {\n      switch (contentType) {\n        case 'markdown':\n          result = frontmatterRegex.markdown.exec(raw)\n          if (result[2]) {\n            return {\n              ...yaml.safeLoad(result[2]),\n              content: result[3]\n            }\n          } else {\n            // Attempt legacy v1 format\n            result = frontmatterRegex.legacy.exec(raw)\n            if (result[2]) {\n              return {\n                title: result[2],\n                description: result[4],\n                content: result[5]\n              }\n            }\n          }\n          break\n        case 'html':\n          result = frontmatterRegex.html.exec(raw)\n          if (result[2]) {\n            return {\n              ...yaml.safeLoad(result[2]),\n              content: result[3]\n            }\n          }\n          break\n      }\n    } catch (err) {\n      WIKI.logger.warn('Failed to parse page metadata. Invalid syntax.')\n    }\n    return {\n      content: raw\n    }\n  }\n\n  /**\n   * Create a New Page\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise of the Page Model Instance\n   */\n  static async createPage(opts) {\n    // -> Validate path\n    if (opts.path.includes('.') || opts.path.includes(' ') || opts.path.includes('\\\\') || opts.path.includes('//')) {\n      throw new WIKI.Error.PageIllegalPath()\n    }\n\n    // -> Remove trailing slash\n    if (opts.path.endsWith('/')) {\n      opts.path = opts.path.slice(0, -1)\n    }\n\n    // -> Remove starting slash\n    if (opts.path.startsWith('/')) {\n      opts.path = opts.path.slice(1)\n    }\n\n    // -> Check for page access\n    if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {\n      locale: opts.locale,\n      path: opts.path\n    })) {\n      throw new WIKI.Error.PageDeleteForbidden()\n    }\n\n    // -> Check for duplicate\n    const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()\n    if (dupCheck) {\n      throw new WIKI.Error.PageDuplicateCreate()\n    }\n\n    // -> Check for empty content\n    if (!opts.content || _.trim(opts.content).length < 1) {\n      throw new WIKI.Error.PageEmptyContent()\n    }\n\n    // -> Format CSS Scripts\n    let scriptCss = ''\n    if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {\n      locale: opts.locale,\n      path: opts.path\n    })) {\n      if (!_.isEmpty(opts.scriptCss)) {\n        scriptCss = new CleanCSS({ inline: false }).minify(opts.scriptCss).styles\n      } else {\n        scriptCss = ''\n      }\n    }\n\n    // -> Format JS Scripts\n    let scriptJs = ''\n    if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {\n      locale: opts.locale,\n      path: opts.path\n    })) {\n      scriptJs = opts.scriptJs || ''\n    }\n\n    // -> Create page\n    await WIKI.models.pages.query().insert({\n      authorId: opts.user.id,\n      content: opts.content,\n      creatorId: opts.user.id,\n      contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),\n      description: opts.description,\n      editorKey: opts.editor,\n      hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }),\n      isPrivate: opts.isPrivate,\n      isPublished: opts.isPublished,\n      localeCode: opts.locale,\n      path: opts.path,\n      publishEndDate: opts.publishEndDate || '',\n      publishStartDate: opts.publishStartDate || '',\n      title: opts.title,\n      toc: '[]',\n      extra: JSON.stringify({\n        js: scriptJs,\n        css: scriptCss\n      })\n    })\n    const page = await WIKI.models.pages.getPageFromDb({\n      path: opts.path,\n      locale: opts.locale,\n      userId: opts.user.id,\n      isPrivate: opts.isPrivate\n    })\n\n    // -> Save Tags\n    if (opts.tags && opts.tags.length > 0) {\n      await WIKI.models.tags.associateTags({ tags: opts.tags, page })\n    }\n\n    // -> Render page to HTML\n    await WIKI.models.pages.renderPage(page)\n\n    // -> Rebuild page tree\n    await WIKI.models.pages.rebuildTree()\n\n    // -> Add to Search Index\n    const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')\n    page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)\n    await WIKI.data.searchEngine.created(page)\n\n    // -> Add to Storage\n    if (!opts.skipStorage) {\n      await WIKI.models.storage.pageEvent({\n        event: 'created',\n        page\n      })\n    }\n\n    // -> Reconnect Links\n    await WIKI.models.pages.reconnectLinks({\n      locale: page.localeCode,\n      path: page.path,\n      mode: 'create'\n    })\n\n    // -> Get latest updatedAt\n    page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)\n\n    return page\n  }\n\n  /**\n   * Update an Existing Page\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise of the Page Model Instance\n   */\n  static async updatePage(opts) {\n    // -> Fetch original page\n    const ogPage = await WIKI.models.pages.query().findById(opts.id)\n    if (!ogPage) {\n      throw new Error('Invalid Page Id')\n    }\n\n    // -> Check for page access\n    if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {\n      locale: ogPage.localeCode,\n      path: ogPage.path\n    })) {\n      throw new WIKI.Error.PageUpdateForbidden()\n    }\n\n    // -> Check for empty content\n    if (!opts.content || _.trim(opts.content).length < 1) {\n      throw new WIKI.Error.PageEmptyContent()\n    }\n\n    // -> Create version snapshot\n    await WIKI.models.pageHistory.addVersion({\n      ...ogPage,\n      isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,\n      action: opts.action ? opts.action : 'updated',\n      versionDate: ogPage.updatedAt\n    })\n\n    // -> Format Extra Properties\n    if (!_.isPlainObject(ogPage.extra)) {\n      ogPage.extra = {}\n    }\n\n    // -> Format CSS Scripts\n    let scriptCss = _.get(ogPage, 'extra.css', '')\n    if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {\n      locale: opts.locale,\n      path: opts.path\n    })) {\n      if (!_.isEmpty(opts.scriptCss)) {\n        scriptCss = new CleanCSS({ inline: false }).minify(opts.scriptCss).styles\n      } else {\n        scriptCss = ''\n      }\n    }\n\n    // -> Format JS Scripts\n    let scriptJs = _.get(ogPage, 'extra.js', '')\n    if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {\n      locale: opts.locale,\n      path: opts.path\n    })) {\n      scriptJs = opts.scriptJs || ''\n    }\n\n    // -> Update page\n    await WIKI.models.pages.query().patch({\n      authorId: opts.user.id,\n      content: opts.content,\n      description: opts.description,\n      isPublished: opts.isPublished === true || opts.isPublished === 1,\n      publishEndDate: opts.publishEndDate || '',\n      publishStartDate: opts.publishStartDate || '',\n      title: opts.title,\n      extra: JSON.stringify({\n        ...ogPage.extra,\n        js: scriptJs,\n        css: scriptCss\n      })\n    }).where('id', ogPage.id)\n    let page = await WIKI.models.pages.getPageFromDb(ogPage.id)\n\n    // -> Save Tags\n    await WIKI.models.tags.associateTags({ tags: opts.tags, page })\n\n    // -> Render page to HTML\n    await WIKI.models.pages.renderPage(page)\n    WIKI.events.outbound.emit('deletePageFromCache', page.hash)\n\n    // -> Update Search Index\n    const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')\n    page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)\n    await WIKI.data.searchEngine.updated(page)\n\n    // -> Update on Storage\n    if (!opts.skipStorage) {\n      await WIKI.models.storage.pageEvent({\n        event: 'updated',\n        page\n      })\n    }\n\n    // -> Perform move?\n    if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {\n      // -> Check target path access\n      if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {\n        locale: opts.locale,\n        path: opts.path\n      })) {\n        throw new WIKI.Error.PageMoveForbidden()\n      }\n\n      await WIKI.models.pages.movePage({\n        id: page.id,\n        destinationLocale: opts.locale,\n        destinationPath: opts.path,\n        user: opts.user\n      })\n    } else {\n      // -> Update title of page tree entry\n      await WIKI.models.knex.table('pageTree').where({\n        pageId: page.id\n      }).update('title', page.title)\n    }\n\n    // -> Get latest updatedAt\n    page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)\n\n    return page\n  }\n\n  /**\n   * Convert an Existing Page\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise of the Page Model Instance\n   */\n  static async convertPage(opts) {\n    // -> Fetch original page\n    const ogPage = await WIKI.models.pages.query().findById(opts.id)\n    if (!ogPage) {\n      throw new Error('Invalid Page Id')\n    }\n\n    if (ogPage.editorKey === opts.editor) {\n      throw new Error('Page is already using this editor. Nothing to convert.')\n    }\n\n    // -> Check for page access\n    if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {\n      locale: ogPage.localeCode,\n      path: ogPage.path\n    })) {\n      throw new WIKI.Error.PageUpdateForbidden()\n    }\n\n    // -> Check content type\n    const sourceContentType = ogPage.contentType\n    const targetContentType = _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text')\n    const shouldConvert = sourceContentType !== targetContentType\n    let convertedContent = null\n\n    // -> Convert content\n    if (shouldConvert) {\n      // -> Markdown => HTML\n      if (sourceContentType === 'markdown' && targetContentType === 'html') {\n        if (!ogPage.render) {\n          throw new Error('Aborted conversion because rendered page content is empty!')\n        }\n        convertedContent = ogPage.render\n\n        const $ = cheerio.load(convertedContent, {\n          decodeEntities: true\n        })\n\n        if ($.root().children().length > 0) {\n          // Remove header anchors\n          $('.toc-anchor').remove()\n\n          // Attempt to convert tabsets\n          $('tabset').each((tabI, tabElm) => {\n            const tabHeaders = []\n            // -> Extract templates\n            $(tabElm).children('template').each((tmplI, tmplElm) => {\n              if ($(tmplElm).attr('v-slot:tabs') === '') {\n                $(tabElm).before('<ul class=\"tabset-headers\">' + $(tmplElm).html() + '</ul>')\n              } else {\n                $(tabElm).after('<div class=\"markdown-tabset\">' + $(tmplElm).html() + '</div>')\n              }\n            })\n            // -> Parse tab headers\n            $(tabElm).prev('.tabset-headers').children((i, elm) => {\n              tabHeaders.push($(elm).html())\n            })\n            $(tabElm).prev('.tabset-headers').remove()\n            // -> Inject tab headers\n            $(tabElm).next('.markdown-tabset').children((i, elm) => {\n              if (tabHeaders.length > i) {\n                $(elm).prepend(`<h2>${tabHeaders[i]}</h2>`)\n              }\n            })\n            $(tabElm).next('.markdown-tabset').prepend('<h1>Tabset</h1>')\n            $(tabElm).remove()\n          })\n\n          convertedContent = $.html('body').replace('<body>', '').replace('</body>', '').replace(/&#x([0-9a-f]{1,6});/ig, (entity, code) => {\n            code = parseInt(code, 16)\n\n            // Don't unescape ASCII characters, assuming they're encoded for a good reason\n            if (code < 0x80) return entity\n\n            return String.fromCodePoint(code)\n          })\n        }\n\n      // -> HTML => Markdown\n      } else if (sourceContentType === 'html' && targetContentType === 'markdown') {\n        const td = new TurndownService({\n          bulletListMarker: '-',\n          codeBlockStyle: 'fenced',\n          emDelimiter: '*',\n          fence: '```',\n          headingStyle: 'atx',\n          hr: '---',\n          linkStyle: 'inlined',\n          preformattedCode: true,\n          strongDelimiter: '**'\n        })\n\n        td.use(turndownPluginGfm)\n\n        td.keep(['kbd'])\n\n        td.addRule('subscript', {\n          filter: ['sub'],\n          replacement: c => `~${c}~`\n        })\n\n        td.addRule('superscript', {\n          filter: ['sup'],\n          replacement: c => `^${c}^`\n        })\n\n        td.addRule('underline', {\n          filter: ['u'],\n          replacement: c => `_${c}_`\n        })\n\n        td.addRule('taskList', {\n          filter: (n, o) => {\n            return n.nodeName === 'INPUT' && n.getAttribute('type') === 'checkbox'\n          },\n          replacement: (c, n) => {\n            return n.getAttribute('checked') ? '[x] ' : '[ ] '\n          }\n        })\n\n        td.addRule('removeTocAnchors', {\n          filter: (n, o) => {\n            return n.nodeName === 'A' && n.classList.contains('toc-anchor')\n          },\n          replacement: c => ''\n        })\n\n        convertedContent = td.turndown(ogPage.content)\n      // -> Unsupported\n      } else {\n        throw new Error('Unsupported source / destination content types combination.')\n      }\n    }\n\n    // -> Create version snapshot\n    if (shouldConvert) {\n      await WIKI.models.pageHistory.addVersion({\n        ...ogPage,\n        isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,\n        action: 'updated',\n        versionDate: ogPage.updatedAt\n      })\n    }\n\n    // -> Update page\n    await WIKI.models.pages.query().patch({\n      contentType: targetContentType,\n      editorKey: opts.editor,\n      ...(convertedContent ? { content: convertedContent } : {})\n    }).where('id', ogPage.id)\n    const page = await WIKI.models.pages.getPageFromDb(ogPage.id)\n\n    await WIKI.models.pages.deletePageFromCache(page.hash)\n    WIKI.events.outbound.emit('deletePageFromCache', page.hash)\n\n    // -> Update on Storage\n    await WIKI.models.storage.pageEvent({\n      event: 'updated',\n      page\n    })\n  }\n\n  /**\n   * Move a Page\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise with no value\n   */\n  static async movePage(opts) {\n    let page\n    if (_.has(opts, 'id')) {\n      page = await WIKI.models.pages.query().findById(opts.id)\n    } else {\n      page = await WIKI.models.pages.query().findOne({\n        path: opts.path,\n        localeCode: opts.locale\n      })\n    }\n    if (!page) {\n      throw new WIKI.Error.PageNotFound()\n    }\n\n    // -> Validate path\n    if (opts.destinationPath.includes('.') || opts.destinationPath.includes(' ') || opts.destinationPath.includes('\\\\') || opts.destinationPath.includes('//')) {\n      throw new WIKI.Error.PageIllegalPath()\n    }\n\n    // -> Remove trailing slash\n    if (opts.destinationPath.endsWith('/')) {\n      opts.destinationPath = opts.destinationPath.slice(0, -1)\n    }\n\n    // -> Remove starting slash\n    if (opts.destinationPath.startsWith('/')) {\n      opts.destinationPath = opts.destinationPath.slice(1)\n    }\n\n    // -> Check for source page access\n    if (!WIKI.auth.checkAccess(opts.user, ['manage:pages'], {\n      locale: page.localeCode,\n      path: page.path\n    })) {\n      throw new WIKI.Error.PageMoveForbidden()\n    }\n    // -> Check for destination page access\n    if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {\n      locale: opts.destinationLocale,\n      path: opts.destinationPath\n    })) {\n      throw new WIKI.Error.PageMoveForbidden()\n    }\n\n    // -> Check for existing page at destination path\n    const destPage = await WIKI.models.pages.query().findOne({\n      path: opts.destinationPath,\n      localeCode: opts.destinationLocale\n    })\n    if (destPage) {\n      throw new WIKI.Error.PagePathCollision()\n    }\n\n    // -> Create version snapshot\n    await WIKI.models.pageHistory.addVersion({\n      ...page,\n      action: 'moved',\n      versionDate: page.updatedAt\n    })\n\n    const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })\n\n    // -> Move page\n    const destinationTitle = (page.title === _.last(page.path.split('/')) ? _.last(opts.destinationPath.split('/')) : page.title)\n    await WIKI.models.pages.query().patch({\n      path: opts.destinationPath,\n      localeCode: opts.destinationLocale,\n      title: destinationTitle,\n      hash: destinationHash\n    }).findById(page.id)\n    await WIKI.models.pages.deletePageFromCache(page.hash)\n    WIKI.events.outbound.emit('deletePageFromCache', page.hash)\n\n    // -> Rebuild page tree\n    await WIKI.models.pages.rebuildTree()\n\n    // -> Rename in Search Index\n    const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')\n    page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)\n    await WIKI.data.searchEngine.renamed({\n      ...page,\n      destinationPath: opts.destinationPath,\n      destinationLocaleCode: opts.destinationLocale,\n      title: destinationTitle,\n      destinationHash\n    })\n\n    // -> Rename in Storage\n    if (!opts.skipStorage) {\n      await WIKI.models.storage.pageEvent({\n        event: 'renamed',\n        page: {\n          ...page,\n          destinationPath: opts.destinationPath,\n          destinationLocaleCode: opts.destinationLocale,\n          destinationHash,\n          moveAuthorId: opts.user.id,\n          moveAuthorName: opts.user.name,\n          moveAuthorEmail: opts.user.email\n        }\n      })\n    }\n\n    // -> Reconnect Links : Changing old links to the new path\n    await WIKI.models.pages.reconnectLinks({\n      sourceLocale: page.localeCode,\n      sourcePath: page.path,\n      locale: opts.destinationLocale,\n      path: opts.destinationPath,\n      mode: 'move'\n    })\n\n    // -> Reconnect Links : Validate invalid links to the new path\n    await WIKI.models.pages.reconnectLinks({\n      locale: opts.destinationLocale,\n      path: opts.destinationPath,\n      mode: 'create'\n    })\n  }\n\n  /**\n   * Delete an Existing Page\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise with no value\n   */\n  static async deletePage(opts) {\n    const page = await WIKI.models.pages.getPageFromDb(_.has(opts, 'id') ? opts.id : opts)\n    if (!page) {\n      throw new WIKI.Error.PageNotFound()\n    }\n\n    // -> Check for page access\n    if (!WIKI.auth.checkAccess(opts.user, ['delete:pages'], {\n      locale: page.locale,\n      path: page.path\n    })) {\n      throw new WIKI.Error.PageDeleteForbidden()\n    }\n\n    // -> Create version snapshot\n    await WIKI.models.pageHistory.addVersion({\n      ...page,\n      action: 'deleted',\n      versionDate: page.updatedAt\n    })\n\n    // -> Delete page\n    await WIKI.models.pages.query().delete().where('id', page.id)\n    await WIKI.models.pages.deletePageFromCache(page.hash)\n    WIKI.events.outbound.emit('deletePageFromCache', page.hash)\n\n    // -> Rebuild page tree\n    await WIKI.models.pages.rebuildTree()\n\n    // -> Delete from Search Index\n    await WIKI.data.searchEngine.deleted(page)\n\n    // -> Delete from Storage\n    if (!opts.skipStorage) {\n      await WIKI.models.storage.pageEvent({\n        event: 'deleted',\n        page\n      })\n    }\n\n    // -> Reconnect Links\n    await WIKI.models.pages.reconnectLinks({\n      locale: page.localeCode,\n      path: page.path,\n      mode: 'delete'\n    })\n  }\n\n  /**\n   * Reconnect links to new/move/deleted page\n   *\n   * @param {Object} opts - Page parameters\n   * @param {string} opts.path - Page Path\n   * @param {string} opts.locale - Page Locale Code\n   * @param {string} [opts.sourcePath] - Previous Page Path (move only)\n   * @param {string} [opts.sourceLocale] - Previous Page Locale Code (move only)\n   * @param {string} opts.mode - Page Update mode (create, move, delete)\n   * @returns {Promise} Promise with no value\n   */\n  static async reconnectLinks (opts) {\n    const pageHref = `/${opts.locale}/${opts.path}`\n    let replaceArgs = {\n      from: '',\n      to: ''\n    }\n    switch (opts.mode) {\n      case 'create':\n        replaceArgs.from = `<a href=\"${pageHref}\" class=\"is-internal-link is-invalid-page\">`\n        replaceArgs.to = `<a href=\"${pageHref}\" class=\"is-internal-link is-valid-page\">`\n        break\n      case 'move':\n        const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}`\n        replaceArgs.from = `<a href=\"${prevPageHref}\" class=\"is-internal-link is-valid-page\">`\n        replaceArgs.to = `<a href=\"${pageHref}\" class=\"is-internal-link is-valid-page\">`\n        break\n      case 'delete':\n        replaceArgs.from = `<a href=\"${pageHref}\" class=\"is-internal-link is-valid-page\">`\n        replaceArgs.to = `<a href=\"${pageHref}\" class=\"is-internal-link is-invalid-page\">`\n        break\n      default:\n        return false\n    }\n\n    let affectedHashes = []\n    // -> Perform replace and return affected page hashes (POSTGRES only)\n    if (WIKI.config.db.type === 'postgres') {\n      const qryHashes = await WIKI.models.pages.query()\n        .returning('hash')\n        .patch({\n          render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])\n        })\n        .whereIn('pages.id', function () {\n          this.select('pageLinks.pageId').from('pageLinks').where({\n            'pageLinks.path': opts.path,\n            'pageLinks.localeCode': opts.locale\n          })\n        })\n      affectedHashes = qryHashes.map(h => h.hash)\n    } else {\n      // -> Perform replace, then query affected page hashes (MYSQL, MARIADB, MSSQL, SQLITE only)\n      await WIKI.models.pages.query()\n        .patch({\n          render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])\n        })\n        .whereIn('pages.id', function () {\n          this.select('pageLinks.pageId').from('pageLinks').where({\n            'pageLinks.path': opts.path,\n            'pageLinks.localeCode': opts.locale\n          })\n        })\n      const qryHashes = await WIKI.models.pages.query()\n        .column('hash')\n        .whereIn('pages.id', function () {\n          this.select('pageLinks.pageId').from('pageLinks').where({\n            'pageLinks.path': opts.path,\n            'pageLinks.localeCode': opts.locale\n          })\n        })\n      affectedHashes = qryHashes.map(h => h.hash)\n    }\n    for (const hash of affectedHashes) {\n      await WIKI.models.pages.deletePageFromCache(hash)\n      WIKI.events.outbound.emit('deletePageFromCache', hash)\n    }\n  }\n\n  /**\n   * Rebuild page tree for new/updated/deleted page\n   *\n   * @returns {Promise} Promise with no value\n   */\n  static async rebuildTree() {\n    const rebuildJob = await WIKI.scheduler.registerJob({\n      name: 'rebuild-tree',\n      immediate: true,\n      worker: true\n    })\n    return rebuildJob.finished\n  }\n\n  /**\n   * Trigger the rendering of a page\n   *\n   * @param {Object} page Page Model Instance\n   * @returns {Promise} Promise with no value\n   */\n  static async renderPage(page) {\n    const renderJob = await WIKI.scheduler.registerJob({\n      name: 'render-page',\n      immediate: true,\n      worker: true\n    }, page.id)\n    return renderJob.finished\n  }\n\n  /**\n   * Fetch an Existing Page from Cache if possible, from DB otherwise and save render to Cache\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise of the Page Model Instance\n   */\n  static async getPage(opts) {\n    // -> Get from cache first\n    let page = await WIKI.models.pages.getPageFromCache(opts)\n    if (!page) {\n      // -> Get from DB\n      page = await WIKI.models.pages.getPageFromDb(opts)\n      if (page) {\n        if (page.render) {\n          // -> Save render to cache\n          await WIKI.models.pages.savePageToCache(page)\n        } else {\n          // -> No render? Last page render failed...\n          throw new Error('Page has no rendered version. Looks like the Last page render failed. Try to edit the page and save it again.')\n        }\n      }\n    }\n    return page\n  }\n\n  /**\n   * Fetch an Existing Page from the Database\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise of the Page Model Instance\n   */\n  static async getPageFromDb(opts) {\n    const queryModeID = _.isNumber(opts)\n    try {\n      return WIKI.models.pages.query()\n        .column([\n          'pages.id',\n          'pages.path',\n          'pages.hash',\n          'pages.title',\n          'pages.description',\n          'pages.isPrivate',\n          'pages.isPublished',\n          'pages.privateNS',\n          'pages.publishStartDate',\n          'pages.publishEndDate',\n          'pages.content',\n          'pages.render',\n          'pages.toc',\n          'pages.contentType',\n          'pages.createdAt',\n          'pages.updatedAt',\n          'pages.editorKey',\n          'pages.localeCode',\n          'pages.authorId',\n          'pages.creatorId',\n          'pages.extra',\n          {\n            authorName: 'author.name',\n            authorEmail: 'author.email',\n            creatorName: 'creator.name',\n            creatorEmail: 'creator.email'\n          }\n        ])\n        .joinRelated('author')\n        .joinRelated('creator')\n        .withGraphJoined('tags')\n        .modifyGraph('tags', builder => {\n          builder.select('tag', 'title')\n        })\n        .where(queryModeID ? {\n          'pages.id': opts\n        } : {\n          'pages.path': opts.path,\n          'pages.localeCode': opts.locale\n        })\n        // .andWhere(builder => {\n        //   if (queryModeID) return\n        //   builder.where({\n        //     'pages.isPublished': true\n        //   }).orWhere({\n        //     'pages.isPublished': false,\n        //     'pages.authorId': opts.userId\n        //   })\n        // })\n        // .andWhere(builder => {\n        //   if (queryModeID) return\n        //   if (opts.isPrivate) {\n        //     builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })\n        //   } else {\n        //     builder.where({ 'pages.isPrivate': false })\n        //   }\n        // })\n        .first()\n    } catch (err) {\n      WIKI.logger.warn(err)\n      throw err\n    }\n  }\n\n  /**\n   * Save a Page Model Instance to Cache\n   *\n   * @param {Object} page Page Model Instance\n   * @returns {Promise} Promise with no value\n   */\n  static async savePageToCache(page) {\n    const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${page.hash}.bin`)\n    await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({\n      id: page.id,\n      authorId: page.authorId,\n      authorName: page.authorName,\n      createdAt: page.createdAt,\n      creatorId: page.creatorId,\n      creatorName: page.creatorName,\n      description: page.description,\n      editorKey: page.editorKey,\n      extra: {\n        css: _.get(page, 'extra.css', ''),\n        js: _.get(page, 'extra.js', '')\n      },\n      isPrivate: page.isPrivate === 1 || page.isPrivate === true,\n      isPublished: page.isPublished === 1 || page.isPublished === true,\n      publishEndDate: page.publishEndDate,\n      publishStartDate: page.publishStartDate,\n      contentType: page.contentType,\n      render: page.render,\n      tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),\n      title: page.title,\n      toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),\n      updatedAt: page.updatedAt\n    }))\n  }\n\n  /**\n   * Fetch an Existing Page from Cache\n   *\n   * @param {Object} opts Page Properties\n   * @returns {Promise} Promise of the Page Model Instance\n   */\n  static async getPageFromCache(opts) {\n    const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })\n    const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${pageHash}.bin`)\n\n    try {\n      const pageBuffer = await fs.readFile(cachePath)\n      let page = WIKI.models.pages.cacheSchema.decode(pageBuffer)\n      return {\n        ...page,\n        path: opts.path,\n        localeCode: opts.locale,\n        isPrivate: opts.isPrivate\n      }\n    } catch (err) {\n      if (err.code === 'ENOENT') {\n        return false\n      }\n      WIKI.logger.error(err)\n      throw err\n    }\n  }\n\n  /**\n   * Delete an Existing Page from Cache\n   *\n   * @param {String} page Page Unique Hash\n   * @returns {Promise} Promise with no value\n   */\n  static async deletePageFromCache(hash) {\n    return fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${hash}.bin`))\n  }\n\n  /**\n   * Flush the contents of the Cache\n   */\n  static async flushCache() {\n    return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache`))\n  }\n\n  /**\n   * Migrate all pages from a source locale to the target locale\n   *\n   * @param {Object} opts Migration properties\n   * @param {string} opts.sourceLocale Source Locale Code\n   * @param {string} opts.targetLocale Target Locale Code\n   * @returns {Promise} Promise with no value\n   */\n  static async migrateToLocale({ sourceLocale, targetLocale }) {\n    return WIKI.models.pages.query()\n      .patch({\n        localeCode: targetLocale\n      })\n      .where({\n        localeCode: sourceLocale\n      })\n      .whereNotExists(function() {\n        this.select('id').from('pages AS pagesm').where('pagesm.localeCode', targetLocale).andWhereRaw('pagesm.path = pages.path')\n      })\n  }\n\n  /**\n   * Clean raw HTML from content for use in search engines\n   *\n   * @param {string} rawHTML Raw HTML\n   * @returns {string} Cleaned Content Text\n   */\n  static cleanHTML(rawHTML = '') {\n    let data = striptags(rawHTML || '', [], ' ')\n      .replace(emojiRegex(), '')\n      // .replace(htmlEntitiesRegex, '')\n    return he.decode(data)\n      .replace(punctuationRegex, ' ')\n      .replace(/(\\r\\n|\\n|\\r)/gm, ' ')\n      .replace(/\\s\\s+/g, ' ')\n      .split(' ').filter(w => w.length > 1).join(' ').toLowerCase()\n  }\n\n  /**\n   * Subscribe to HA propagation events\n   */\n  static subscribeToEvents() {\n    WIKI.events.inbound.on('deletePageFromCache', hash => {\n      WIKI.models.pages.deletePageFromCache(hash)\n    })\n    WIKI.events.inbound.on('flushCache', () => {\n      WIKI.models.pages.flushCache()\n    })\n  }\n}\n"
  },
  {
    "path": "server/models/renderers.js",
    "content": "const Model = require('objection').Model\nconst path = require('path')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst DepGraph = require('dependency-graph').DepGraph\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * Renderer model\n */\nmodule.exports = class Renderer extends Model {\n  static get tableName() { return 'renderers' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config']\n  }\n\n  static async getRenderers() {\n    return WIKI.models.renderers.query()\n  }\n\n  static async fetchDefinitions() {\n    const rendererDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/rendering'))\n    let diskRenderers = []\n    for (let dir of rendererDirs) {\n      const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/rendering', dir, 'definition.yml'), 'utf8')\n      diskRenderers.push(yaml.safeLoad(def))\n    }\n    WIKI.data.renderers = diskRenderers.map(renderer => ({\n      ...renderer,\n      props: commonHelper.parseModuleProps(renderer.props)\n    }))\n  }\n\n  static async refreshRenderersFromDisk() {\n    let trx\n    try {\n      const dbRenderers = await WIKI.models.renderers.query()\n\n      // -> Fetch definitions from disk\n      await WIKI.models.renderers.fetchDefinitions()\n\n      // -> Insert new Renderers\n      let newRenderers = []\n      for (let renderer of WIKI.data.renderers) {\n        if (!_.some(dbRenderers, ['key', renderer.key])) {\n          newRenderers.push({\n            key: renderer.key,\n            isEnabled: _.get(renderer, 'enabledDefault', true),\n            config: _.transform(renderer.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {})\n          })\n        } else {\n          const rendererConfig = _.get(_.find(dbRenderers, ['key', renderer.key]), 'config', {})\n          await WIKI.models.renderers.query().patch({\n            config: _.transform(renderer.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, rendererConfig)\n          }).where('key', renderer.key)\n        }\n      }\n      if (newRenderers.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let renderer of newRenderers) {\n          await WIKI.models.renderers.query(trx).insert(renderer)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newRenderers.length} new renderers: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new renderers found: [ SKIPPED ]`)\n      }\n\n      // -> Delete removed Renderers\n      for (const renderer of dbRenderers) {\n        if (!_.some(WIKI.data.renderers, ['key', renderer.key])) {\n          await WIKI.models.renderers.query().where('key', renderer.key).del()\n          WIKI.logger.info(`Removed renderer ${renderer.key} because it is no longer present in the modules folder: [ OK ]`)\n        }\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new renderers: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  static async getRenderingPipeline(contentType) {\n    const renderersDb = await WIKI.models.renderers.query().where('isEnabled', true)\n    if (renderersDb && renderersDb.length > 0) {\n      const renderers = renderersDb.map(rdr => {\n        const renderer = _.find(WIKI.data.renderers, ['key', rdr.key])\n        return {\n          ...renderer,\n          config: rdr.config\n        }\n      })\n\n      // Build tree\n      const rawCores = _.filter(renderers, renderer => !_.has(renderer, 'dependsOn')).map(core => {\n        core.children = _.filter(renderers, ['dependsOn', core.key])\n        return core\n      })\n\n      // Build dependency graph\n      const graph = new DepGraph({ circular: true })\n      rawCores.map(core => { graph.addNode(core.key) })\n      rawCores.map(core => {\n        rawCores.map(coreTarget => {\n          if (core.key !== coreTarget.key) {\n            if (core.output === coreTarget.input) {\n              graph.addDependency(core.key, coreTarget.key)\n            }\n          }\n        })\n      })\n\n      // Filter unused cores\n      let activeCoreKeys = _.filter(rawCores, ['input', contentType]).map(core => core.key)\n      _.clone(activeCoreKeys).map(coreKey => {\n        activeCoreKeys = _.union(activeCoreKeys, graph.dependenciesOf(coreKey))\n      })\n      const activeCores = _.filter(rawCores, core => _.includes(activeCoreKeys, core.key))\n\n      // Rebuild dependency graph with active cores\n      const graphActive = new DepGraph({ circular: true })\n      activeCores.map(core => { graphActive.addNode(core.key) })\n      activeCores.map(core => {\n        activeCores.map(coreTarget => {\n          if (core.key !== coreTarget.key) {\n            if (core.output === coreTarget.input) {\n              graphActive.addDependency(core.key, coreTarget.key)\n            }\n          }\n        })\n      })\n\n      // Reorder cores in reverse dependency order\n      let orderedCores = []\n      _.reverse(graphActive.overallOrder()).map(coreKey => {\n        orderedCores.push(_.find(rawCores, ['key', coreKey]))\n      })\n\n      return orderedCores\n    } else {\n      WIKI.logger.error(`Rendering pipeline is empty!`)\n      return false\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/searchEngines.js",
    "content": "const Model = require('objection').Model\nconst path = require('path')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * SearchEngine model\n */\nmodule.exports = class SearchEngine extends Model {\n  static get tableName() { return 'searchEngines' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'},\n        level: {type: 'string'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config']\n  }\n\n  static async getSearchEngines() {\n    return WIKI.models.searchEngines.query()\n  }\n\n  static async refreshSearchEnginesFromDisk() {\n    let trx\n    try {\n      const dbSearchEngines = await WIKI.models.searchEngines.query()\n\n      // -> Fetch definitions from disk\n      const searchEnginesDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/search'))\n      let diskSearchEngines = []\n      for (let dir of searchEnginesDirs) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/search', dir, 'definition.yml'), 'utf8')\n        diskSearchEngines.push(yaml.safeLoad(def))\n      }\n      WIKI.data.searchEngines = diskSearchEngines.map(searchEngine => ({\n        ...searchEngine,\n        props: commonHelper.parseModuleProps(searchEngine.props)\n      }))\n\n      // -> Insert new searchEngines\n      let newSearchEngines = []\n      for (let searchEngine of WIKI.data.searchEngines) {\n        if (!_.some(dbSearchEngines, ['key', searchEngine.key])) {\n          newSearchEngines.push({\n            key: searchEngine.key,\n            isEnabled: false,\n            config: _.transform(searchEngine.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {})\n          })\n        } else {\n          const searchEngineConfig = _.get(_.find(dbSearchEngines, ['key', searchEngine.key]), 'config', {})\n          await WIKI.models.searchEngines.query().patch({\n            config: _.transform(searchEngine.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, searchEngineConfig)\n          }).where('key', searchEngine.key)\n        }\n      }\n      if (newSearchEngines.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let searchEngine of newSearchEngines) {\n          await WIKI.models.searchEngines.query(trx).insert(searchEngine)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newSearchEngines.length} new search engines: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new search engines found: [ SKIPPED ]`)\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new search engines: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  static async initEngine({ activate = false } = {}) {\n    const searchEngine = await WIKI.models.searchEngines.query().findOne('isEnabled', true)\n    if (searchEngine) {\n      WIKI.data.searchEngine = require(`../modules/search/${searchEngine.key}/engine`)\n      WIKI.data.searchEngine.key = searchEngine.key\n      WIKI.data.searchEngine.config = searchEngine.config\n      if (activate) {\n        try {\n          await WIKI.data.searchEngine.activate()\n        } catch (err) {\n          // -> Revert to basic engine\n          if (err instanceof WIKI.Error.SearchActivationFailed) {\n            await WIKI.models.searchEngines.query().patch({ isEnabled: false }).where('key', searchEngine.key)\n            await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')\n            await WIKI.models.searchEngines.initEngine()\n          }\n          throw err\n        }\n      }\n\n      try {\n        await WIKI.data.searchEngine.init()\n      } catch (err) {\n        WIKI.logger.warn(err)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/settings.js",
    "content": "const Model = require('objection').Model\nconst _ = require('lodash')\n\n/* global WIKI */\n\n/**\n * Settings model\n */\nmodule.exports = class Setting extends Model {\n  static get tableName() { return 'settings' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key'],\n\n      properties: {\n        key: {type: 'string'},\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['value']\n  }\n\n  $beforeUpdate() {\n    this.updatedAt = new Date().toISOString()\n  }\n  $beforeInsert() {\n    this.updatedAt = new Date().toISOString()\n  }\n\n  static async getConfig() {\n    const settings = await WIKI.models.settings.query()\n    if (settings.length > 0) {\n      return _.reduce(settings, (res, val, key) => {\n        _.set(res, val.key, (_.has(val.value, 'v')) ? val.value.v : val.value)\n        return res\n      }, {})\n    } else {\n      return false\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/storage.js",
    "content": "const Model = require('objection').Model\nconst path = require('path')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst yaml = require('js-yaml')\nconst commonHelper = require('../helpers/common')\n\n/* global WIKI */\n\n/**\n * Storage model\n */\nmodule.exports = class Storage extends Model {\n  static get tableName() { return 'storage' }\n  static get idColumn() { return 'key' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['key', 'isEnabled'],\n\n      properties: {\n        key: {type: 'string'},\n        isEnabled: {type: 'boolean'},\n        mode: {type: 'string'}\n      }\n    }\n  }\n\n  static get jsonAttributes() {\n    return ['config', 'state']\n  }\n\n  static async getTargets() {\n    return WIKI.models.storage.query()\n  }\n\n  static async refreshTargetsFromDisk() {\n    let trx\n    try {\n      const dbTargets = await WIKI.models.storage.query()\n\n      // -> Fetch definitions from disk\n      const storageDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/storage'))\n      let diskTargets = []\n      for (let dir of storageDirs) {\n        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/storage', dir, 'definition.yml'), 'utf8')\n        diskTargets.push(yaml.safeLoad(def))\n      }\n      WIKI.data.storage = diskTargets.map(target => ({\n        ...target,\n        isAvailable: _.get(target, 'isAvailable', false),\n        props: commonHelper.parseModuleProps(target.props)\n      }))\n\n      // -> Insert new targets\n      let newTargets = []\n      for (let target of WIKI.data.storage) {\n        if (!_.some(dbTargets, ['key', target.key])) {\n          newTargets.push({\n            key: target.key,\n            isEnabled: false,\n            mode: target.defaultMode || 'push',\n            syncInterval: target.schedule || 'P0D',\n            config: _.transform(target.props, (result, value, key) => {\n              _.set(result, key, value.default)\n              return result\n            }, {}),\n            state: {\n              status: 'pending',\n              message: '',\n              lastAttempt: null\n            }\n          })\n        } else {\n          const targetConfig = _.get(_.find(dbTargets, ['key', target.key]), 'config', {})\n          await WIKI.models.storage.query().patch({\n            config: _.transform(target.props, (result, value, key) => {\n              if (!_.has(result, key)) {\n                _.set(result, key, value.default)\n              }\n              return result\n            }, targetConfig)\n          }).where('key', target.key)\n        }\n      }\n      if (newTargets.length > 0) {\n        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)\n        for (let target of newTargets) {\n          await WIKI.models.storage.query(trx).insert(target)\n        }\n        await trx.commit()\n        WIKI.logger.info(`Loaded ${newTargets.length} new storage targets: [ OK ]`)\n      } else {\n        WIKI.logger.info(`No new storage targets found: [ SKIPPED ]`)\n      }\n\n      // -> Delete removed targets\n      for (const target of dbTargets) {\n        if (!_.some(WIKI.data.storage, ['key', target.key])) {\n          await WIKI.models.storage.query().where('key', target.key).del()\n          WIKI.logger.info(`Removed target ${target.key} because it is no longer present in the modules folder: [ OK ]`)\n        }\n      }\n    } catch (err) {\n      WIKI.logger.error(`Failed to scan or load new storage providers: [ FAILED ]`)\n      WIKI.logger.error(err)\n      if (trx) {\n        trx.rollback()\n      }\n    }\n  }\n\n  /**\n   * Initialize active storage targets\n   */\n  static async initTargets() {\n    this.targets = await WIKI.models.storage.query().where('isEnabled', true).orderBy('key')\n    try {\n      // -> Stop and delete existing jobs\n      const prevjobs = _.remove(WIKI.scheduler.jobs, job => job.name === `sync-storage`)\n      if (prevjobs.length > 0) {\n        prevjobs.forEach(job => job.stop())\n      }\n\n      // -> Initialize targets\n      for (let target of this.targets) {\n        const targetDef = _.find(WIKI.data.storage, ['key', target.key])\n        target.fn = require(`../modules/storage/${target.key}/storage`)\n        target.fn.config = target.config\n        target.fn.mode = target.mode\n        try {\n          await target.fn.init()\n\n          // -> Save succeeded init state\n          await WIKI.models.storage.query().patch({\n            state: {\n              status: 'operational',\n              message: '',\n              lastAttempt: new Date().toISOString()\n            }\n          }).where('key', target.key)\n\n          // -> Set recurring sync job\n          if (targetDef.schedule && target.syncInterval !== `P0D`) {\n            WIKI.scheduler.registerJob({\n              name: `sync-storage`,\n              immediate: false,\n              schedule: target.syncInterval,\n              repeat: true\n            }, target.key)\n          }\n\n          // -> Set internal recurring sync job\n          if (targetDef.internalSchedule && targetDef.internalSchedule !== `P0D`) {\n            WIKI.scheduler.registerJob({\n              name: `sync-storage`,\n              immediate: false,\n              schedule: target.internalSchedule,\n              repeat: true\n            }, target.key)\n          }\n        } catch (err) {\n          // -> Save initialization error\n          await WIKI.models.storage.query().patch({\n            state: {\n              status: 'error',\n              message: err.message,\n              lastAttempt: new Date().toISOString()\n            }\n          }).where('key', target.key)\n        }\n      }\n    } catch (err) {\n      WIKI.logger.warn(err)\n      throw err\n    }\n  }\n\n  static async pageEvent({ event, page }) {\n    try {\n      for (let target of this.targets) {\n        await target.fn[event](page)\n      }\n    } catch (err) {\n      WIKI.logger.warn(err)\n      throw err\n    }\n  }\n\n  static async assetEvent({ event, asset }) {\n    try {\n      for (let target of this.targets) {\n        await target.fn[`asset${_.capitalize(event)}`](asset)\n      }\n    } catch (err) {\n      WIKI.logger.warn(err)\n      throw err\n    }\n  }\n\n  static async getLocalLocations({ asset }) {\n    const locations = []\n    const promises = this.targets.map(async (target) => {\n      try {\n        const path = await target.fn.getLocalLocation(asset)\n        locations.push({\n          path,\n          key: target.key\n        })\n      } catch (err) {\n        WIKI.logger.warn(err)\n      }\n    })\n    await Promise.all(promises)\n    return locations\n  }\n\n  static async executeAction(targetKey, handler) {\n    try {\n      const target = _.find(this.targets, ['key', targetKey])\n      if (target) {\n        if (_.hasIn(target.fn, handler)) {\n          await target.fn[handler]()\n        } else {\n          throw new Error('Invalid Handler for Storage Target')\n        }\n      } else {\n        throw new Error('Invalid or Inactive Storage Target')\n      }\n    } catch (err) {\n      WIKI.logger.warn(err)\n      throw err\n    }\n  }\n}\n"
  },
  {
    "path": "server/models/tags.js",
    "content": "const Model = require('objection').Model\nconst _ = require('lodash')\n\n/* global WIKI */\n\n/**\n * Tags model\n */\nmodule.exports = class Tag extends Model {\n  static get tableName() { return 'tags' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['tag'],\n\n      properties: {\n        id: {type: 'integer'},\n        tag: {type: 'string'},\n        title: {type: 'string'},\n\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      pages: {\n        relation: Model.ManyToManyRelation,\n        modelClass: require('./pages'),\n        join: {\n          from: 'tags.id',\n          through: {\n            from: 'pageTags.tagId',\n            to: 'pageTags.pageId'\n          },\n          to: 'pages.id'\n        }\n      }\n    }\n  }\n\n  $beforeUpdate() {\n    this.updatedAt = new Date().toISOString()\n  }\n  $beforeInsert() {\n    this.createdAt = new Date().toISOString()\n    this.updatedAt = new Date().toISOString()\n  }\n\n  static async associateTags ({ tags, page }) {\n    let existingTags = await WIKI.models.tags.query().column('id', 'tag')\n\n    // Format tags\n\n    tags = _.uniq(tags.map(t => _.trim(t).toLowerCase()))\n\n    // Create missing tags\n\n    const newTags = _.filter(tags, t => !_.some(existingTags, ['tag', t])).map(t => ({\n      tag: t,\n      title: t\n    }))\n    if (newTags.length > 0) {\n      if (WIKI.config.db.type === 'postgres') {\n        const createdTags = await WIKI.models.tags.query().insert(newTags)\n        existingTags = _.concat(existingTags, createdTags)\n      } else {\n        for (const newTag of newTags) {\n          const createdTag = await WIKI.models.tags.query().insert(newTag)\n          existingTags.push(createdTag)\n        }\n      }\n    }\n\n    // Fetch current page tags\n\n    const targetTags = _.filter(existingTags, t => _.includes(tags, t.tag))\n    const currentTags = await page.$relatedQuery('tags')\n\n    // Tags to relate\n\n    const tagsToRelate = _.differenceBy(targetTags, currentTags, 'id')\n    if (tagsToRelate.length > 0) {\n      if (WIKI.config.db.type === 'postgres') {\n        await page.$relatedQuery('tags').relate(tagsToRelate)\n      } else {\n        for (const tag of tagsToRelate) {\n          await page.$relatedQuery('tags').relate(tag)\n        }\n      }\n    }\n\n    // Tags to unrelate\n\n    const tagsToUnrelate = _.differenceBy(currentTags, targetTags, 'id')\n    if (tagsToUnrelate.length > 0) {\n      await page.$relatedQuery('tags').unrelate().whereIn('tags.id', _.map(tagsToUnrelate, 'id'))\n    }\n\n    page.tags = targetTags\n  }\n}\n"
  },
  {
    "path": "server/models/userKeys.js",
    "content": "/* global WIKI */\n\nconst Model = require('objection').Model\nconst { DateTime } = require('luxon')\nconst { nanoid } = require('nanoid')\n\n/**\n * Users model\n */\nmodule.exports = class UserKey extends Model {\n  static get tableName() { return 'userKeys' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['kind', 'token', 'validUntil'],\n\n      properties: {\n        id: {type: 'integer'},\n        kind: {type: 'string'},\n        token: {type: 'string'},\n        createdAt: {type: 'string'},\n        validUntil: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      user: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./users'),\n        join: {\n          from: 'userKeys.userId',\n          to: 'users.id'\n        }\n      }\n    }\n  }\n\n  async $beforeInsert(context) {\n    await super.$beforeInsert(context)\n\n    this.createdAt = DateTime.utc().toISO()\n  }\n\n  static async generateToken ({ userId, kind }, context) {\n    const token = await nanoid()\n    await WIKI.models.userKeys.query().insert({\n      kind,\n      token,\n      validUntil: DateTime.utc().plus({ days: 1 }).toISO(),\n      userId\n    })\n    return token\n  }\n\n  static async validateToken ({ kind, token, skipDelete }, context) {\n    const res = await WIKI.models.userKeys.query().findOne({ kind, token }).withGraphJoined('user')\n    if (res) {\n      if (skipDelete !== true) {\n        await WIKI.models.userKeys.query().deleteById(res.id)\n      }\n      if (DateTime.utc() > DateTime.fromISO(res.validUntil)) {\n        throw new WIKI.Error.AuthValidationTokenInvalid()\n      }\n      return res.user\n    } else {\n      throw new WIKI.Error.AuthValidationTokenInvalid()\n    }\n  }\n\n  static async destroyToken ({ token }) {\n    return WIKI.models.userKeys.query().findOne({ token }).delete()\n  }\n}\n"
  },
  {
    "path": "server/models/users.js",
    "content": "/* global WIKI */\n\nconst bcrypt = require('bcryptjs-then')\nconst _ = require('lodash')\nconst tfa = require('node-2fa')\nconst jwt = require('jsonwebtoken')\nconst Model = require('objection').Model\nconst validate = require('validate.js')\nconst qr = require('qr-image')\n\nconst bcryptRegexp = /^\\$2[ayb]\\$[0-9]{2}\\$[A-Za-z0-9./]{53}$/\n\n/**\n * Users model\n */\nmodule.exports = class User extends Model {\n  static get tableName() { return 'users' }\n\n  static get jsonSchema () {\n    return {\n      type: 'object',\n      required: ['email'],\n\n      properties: {\n        id: {type: 'integer'},\n        email: {type: 'string', format: 'email'},\n        name: {type: 'string', minLength: 1, maxLength: 255},\n        providerId: {type: 'string'},\n        password: {type: 'string'},\n        tfaIsActive: {type: 'boolean', default: false},\n        tfaSecret: {type: ['string', null]},\n        jobTitle: {type: 'string'},\n        location: {type: 'string'},\n        pictureUrl: {type: 'string'},\n        isSystem: {type: 'boolean'},\n        isActive: {type: 'boolean'},\n        isVerified: {type: 'boolean'},\n        createdAt: {type: 'string'},\n        updatedAt: {type: 'string'}\n      }\n    }\n  }\n\n  static get relationMappings() {\n    return {\n      groups: {\n        relation: Model.ManyToManyRelation,\n        modelClass: require('./groups'),\n        join: {\n          from: 'users.id',\n          through: {\n            from: 'userGroups.userId',\n            to: 'userGroups.groupId'\n          },\n          to: 'groups.id'\n        }\n      },\n      provider: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./authentication'),\n        join: {\n          from: 'users.providerKey',\n          to: 'authentication.key'\n        }\n      },\n      defaultEditor: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./editors'),\n        join: {\n          from: 'users.editorKey',\n          to: 'editors.key'\n        }\n      },\n      locale: {\n        relation: Model.BelongsToOneRelation,\n        modelClass: require('./locales'),\n        join: {\n          from: 'users.localeCode',\n          to: 'locales.code'\n        }\n      }\n    }\n  }\n\n  async $beforeUpdate(opt, context) {\n    await super.$beforeUpdate(opt, context)\n\n    this.updatedAt = new Date().toISOString()\n\n    if (!(opt.patch && this.password === undefined)) {\n      await this.generateHash()\n    }\n  }\n  async $beforeInsert(context) {\n    await super.$beforeInsert(context)\n\n    this.createdAt = new Date().toISOString()\n    this.updatedAt = new Date().toISOString()\n\n    await this.generateHash()\n  }\n\n  // ------------------------------------------------\n  // Instance Methods\n  // ------------------------------------------------\n\n  async generateHash() {\n    if (this.password) {\n      if (bcryptRegexp.test(this.password)) { return }\n      this.password = await bcrypt.hash(this.password, 12)\n    }\n  }\n\n  async verifyPassword(pwd) {\n    if (await bcrypt.compare(pwd, this.password) === true) {\n      return true\n    } else {\n      throw new WIKI.Error.AuthLoginFailed()\n    }\n  }\n\n  async generateTFA() {\n    let tfaInfo = tfa.generateSecret({\n      name: WIKI.config.title,\n      account: this.email\n    })\n    await WIKI.models.users.query().findById(this.id).patch({\n      tfaIsActive: false,\n      tfaSecret: tfaInfo.secret\n    })\n    const safeTitle = WIKI.config.title.replace(/[\\s-.,=!@#$%?&*()+[\\]{}/\\\\;<>]/g, '')\n    return qr.imageSync(`otpauth://totp/${safeTitle}:${this.email}?secret=${tfaInfo.secret}`, { type: 'svg' })\n  }\n\n  async enableTFA() {\n    return WIKI.models.users.query().findById(this.id).patch({\n      tfaIsActive: true\n    })\n  }\n\n  async disableTFA() {\n    return this.$query.patch({\n      tfaIsActive: false,\n      tfaSecret: ''\n    })\n  }\n\n  verifyTFA(code) {\n    let result = tfa.verifyToken(this.tfaSecret, code)\n    return (result && _.has(result, 'delta') && result.delta === 0)\n  }\n\n  getGlobalPermissions() {\n    return _.uniq(_.flatten(_.map(this.groups, 'permissions')))\n  }\n\n  getGroups() {\n    return _.uniq(_.map(this.groups, 'id'))\n  }\n\n  // ------------------------------------------------\n  // Model Methods\n  // ------------------------------------------------\n\n  static async processProfile({ profile, providerKey }) {\n    const provider = _.get(WIKI.auth.strategies, providerKey, {})\n    provider.info = _.find(WIKI.data.authentication, ['key', provider.stategyKey])\n\n    // Find existing user\n    let user = await WIKI.models.users.query().findOne({\n      providerId: _.toString(profile.id),\n      providerKey\n    })\n\n    // Parse email\n    let primaryEmail = ''\n    if (_.isArray(profile.emails)) {\n      const e = _.find(profile.emails, ['primary', true])\n      primaryEmail = (e) ? e.value : _.first(profile.emails).value\n    } else if (_.isArray(profile.email)) {\n      primaryEmail = _.first(_.flattenDeep([profile.email]))\n    } else if (_.isString(profile.email) && profile.email.length > 5) {\n      primaryEmail = profile.email\n    } else if (_.isString(profile.mail) && profile.mail.length > 5) {\n      primaryEmail = profile.mail\n    } else if (profile.user && profile.user.email && profile.user.email.length > 5) {\n      primaryEmail = profile.user.email\n    } else {\n      throw new Error('Missing or invalid email address from profile.')\n    }\n    primaryEmail = _.toLower(primaryEmail)\n\n    // Find pending social user\n    if (!user) {\n      user = await WIKI.models.users.query().findOne({\n        email: primaryEmail,\n        providerId: null,\n        providerKey\n      })\n      if (user) {\n        user = await user.$query().patchAndFetch({\n          providerId: _.toString(profile.id)\n        })\n      }\n    }\n\n    // Parse display name\n    let displayName = ''\n    if (_.isString(profile.displayName) && profile.displayName.length > 0) {\n      displayName = profile.displayName\n    } else if (_.isString(profile.name) && profile.name.length > 0) {\n      displayName = profile.name\n    } else {\n      displayName = primaryEmail.split('@')[0]\n    }\n\n    // Parse picture URL / Data\n    let pictureUrl = ''\n    if (profile.picture && Buffer.isBuffer(profile.picture)) {\n      pictureUrl = 'internal'\n    } else {\n      pictureUrl = _.truncate(_.get(profile, 'picture', _.get(user, 'pictureUrl', null)), {\n        length: 255,\n        omission: ''\n      })\n    }\n\n    // Update existing user\n    if (user) {\n      if (!user.isActive) {\n        throw new WIKI.Error.AuthAccountBanned()\n      }\n      if (user.isSystem) {\n        throw new Error('This is a system reserved account and cannot be used.')\n      }\n\n      user = await user.$query().patchAndFetch({\n        email: primaryEmail,\n        name: displayName,\n        pictureUrl: pictureUrl\n      })\n\n      if (pictureUrl === 'internal') {\n        await WIKI.models.users.updateUserAvatarData(user.id, profile.picture)\n      }\n\n      return user\n    }\n\n    // Self-registration\n    if (provider.selfRegistration) {\n      // Check if email domain is whitelisted\n      if (_.get(provider, 'domainWhitelist', []).length > 0) {\n        const emailDomain = _.last(primaryEmail.split('@'))\n        if (!_.includes(provider.domainWhitelist, emailDomain)) {\n          throw new WIKI.Error.AuthRegistrationDomainUnauthorized()\n        }\n      }\n\n      // Create account\n      user = await WIKI.models.users.query().insertAndFetch({\n        providerKey: providerKey,\n        providerId: _.toString(profile.id),\n        email: primaryEmail,\n        name: displayName,\n        pictureUrl: pictureUrl,\n        localeCode: WIKI.config.lang.code,\n        defaultEditor: 'markdown',\n        tfaIsActive: false,\n        isSystem: false,\n        isActive: true,\n        isVerified: true\n      })\n\n      // Assign to group(s)\n      if (provider.autoEnrollGroups.length > 0) {\n        await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)\n      }\n\n      if (pictureUrl === 'internal') {\n        await WIKI.models.users.updateUserAvatarData(user.id, profile.picture)\n      }\n\n      return user\n    }\n\n    throw new Error('You are not authorized to login.')\n  }\n\n  /**\n   * Login a user\n   */\n  static async login (opts, context) {\n    if (_.has(WIKI.auth.strategies, opts.strategy)) {\n      const selStrategy = _.get(WIKI.auth.strategies, opts.strategy)\n      if (!selStrategy.isEnabled) {\n        throw new WIKI.Error.AuthProviderInvalid()\n      }\n\n      const strInfo = _.find(WIKI.data.authentication, ['key', selStrategy.strategyKey])\n\n      // Inject form user/pass\n      if (strInfo.useForm) {\n        _.set(context.req, 'body.email', opts.username)\n        _.set(context.req, 'body.password', opts.password)\n        _.set(context.req.params, 'strategy', opts.strategy)\n      }\n\n      // Authenticate\n      return new Promise((resolve, reject) => {\n        WIKI.auth.passport.authenticate(selStrategy.key, {\n          session: !strInfo.useForm,\n          scope: strInfo.scopes ? strInfo.scopes : null\n        }, async (err, user, info) => {\n          if (err) { return reject(err) }\n          if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }\n\n          try {\n            const resp = await WIKI.models.users.afterLoginChecks(user, context, {\n              skipTFA: !strInfo.useForm,\n              skipChangePwd: !strInfo.useForm\n            })\n            resolve(resp)\n          } catch (err) {\n            reject(err)\n          }\n        })(context.req, context.res, () => {})\n      })\n    } else {\n      throw new WIKI.Error.AuthProviderInvalid()\n    }\n  }\n\n  /**\n   * Perform post-login checks\n   */\n  static async afterLoginChecks (user, context, { skipTFA, skipChangePwd } = { skipTFA: false, skipChangePwd: false }) {\n    // Get redirect target\n    user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions', 'redirectOnLogin')\n    let redirect = '/'\n    if (user.groups && user.groups.length > 0) {\n      for (const grp of user.groups) {\n        if (!_.isEmpty(grp.redirectOnLogin) && grp.redirectOnLogin !== '/') {\n          redirect = grp.redirectOnLogin\n          break\n        }\n      }\n    }\n\n    // Is 2FA required?\n    if (!skipTFA) {\n      if (user.tfaIsActive && user.tfaSecret) {\n        try {\n          const tfaToken = await WIKI.models.userKeys.generateToken({\n            kind: 'tfa',\n            userId: user.id\n          })\n          return {\n            mustProvideTFA: true,\n            continuationToken: tfaToken,\n            redirect\n          }\n        } catch (errc) {\n          WIKI.logger.warn(errc)\n          throw new WIKI.Error.AuthGenericError()\n        }\n      } else if (WIKI.config.auth.enforce2FA || (user.tfaIsActive && !user.tfaSecret)) {\n        try {\n          const tfaQRImage = await user.generateTFA()\n          const tfaToken = await WIKI.models.userKeys.generateToken({\n            kind: 'tfaSetup',\n            userId: user.id\n          })\n          return {\n            mustSetupTFA: true,\n            continuationToken: tfaToken,\n            tfaQRImage,\n            redirect\n          }\n        } catch (errc) {\n          WIKI.logger.warn(errc)\n          throw new WIKI.Error.AuthGenericError()\n        }\n      }\n    }\n\n    // Must Change Password?\n    if (!skipChangePwd && user.mustChangePwd) {\n      try {\n        const pwdChangeToken = await WIKI.models.userKeys.generateToken({\n          kind: 'changePwd',\n          userId: user.id\n        })\n\n        return {\n          mustChangePwd: true,\n          continuationToken: pwdChangeToken,\n          redirect\n        }\n      } catch (errc) {\n        WIKI.logger.warn(errc)\n        throw new WIKI.Error.AuthGenericError()\n      }\n    }\n\n    return new Promise((resolve, reject) => {\n      context.req.login(user, { session: false }, async errc => {\n        if (errc) { return reject(errc) }\n        const jwtToken = await WIKI.models.users.refreshToken(user)\n        resolve({ jwt: jwtToken.token, redirect })\n      })\n    })\n  }\n\n  /**\n   * Generate a new token for a user\n   */\n  static async refreshToken(user) {\n    if (_.isSafeInteger(user)) {\n      user = await WIKI.models.users.query().findById(user).withGraphFetched('groups').modifyGraph('groups', builder => {\n        builder.select('groups.id', 'permissions')\n      })\n      if (!user) {\n        WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)\n        throw new WIKI.Error.AuthGenericError()\n      }\n      if (!user.isActive) {\n        WIKI.logger.warn(`Failed to refresh token for user ${user}: Inactive.`)\n        throw new WIKI.Error.AuthAccountBanned()\n      }\n    } else if (_.isNil(user.groups)) {\n      user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions')\n    }\n\n    // Update Last Login Date\n    // -> Bypass Objection.js to avoid updating the updatedAt field\n    await WIKI.models.knex('users').where('id', user.id).update({ lastLoginAt: new Date().toISOString() })\n\n    return {\n      token: jwt.sign({\n        id: user.id,\n        email: user.email,\n        name: user.name,\n        av: user.pictureUrl,\n        tz: user.timezone,\n        lc: user.localeCode,\n        df: user.dateFormat,\n        ap: user.appearance,\n        // defaultEditor: user.defaultEditor,\n        permissions: user.getGlobalPermissions(),\n        groups: user.getGroups()\n      }, {\n        key: WIKI.config.certs.private,\n        passphrase: WIKI.config.sessionSecret\n      }, {\n        algorithm: 'RS256',\n        expiresIn: WIKI.config.auth.tokenExpiration,\n        audience: WIKI.config.auth.audience,\n        issuer: 'urn:wiki.js'\n      }),\n      user\n    }\n  }\n\n  /**\n   * Verify a TFA login\n   */\n  static async loginTFA ({ securityCode, continuationToken, setup }, context) {\n    if (securityCode.length === 6 && continuationToken.length > 1) {\n      const user = await WIKI.models.userKeys.validateToken({\n        kind: setup ? 'tfaSetup' : 'tfa',\n        token: continuationToken,\n        skipDelete: setup\n      })\n      if (user) {\n        if (user.verifyTFA(securityCode)) {\n          if (setup) {\n            await user.enableTFA()\n          }\n          return WIKI.models.users.afterLoginChecks(user, context, { skipTFA: true })\n        } else {\n          throw new WIKI.Error.AuthTFAFailed()\n        }\n      }\n    }\n    throw new WIKI.Error.AuthTFAInvalid()\n  }\n\n  /**\n   * Change Password from a Mandatory Password Change after Login\n   */\n  static async loginChangePassword ({ continuationToken, newPassword }, context) {\n    if (!newPassword || newPassword.length < 6) {\n      throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')\n    }\n    const usr = await WIKI.models.userKeys.validateToken({\n      kind: 'changePwd',\n      token: continuationToken\n    })\n\n    if (usr) {\n      if (!usr.isActive) {\n        throw new WIKI.Error.AuthAccountBanned()\n      }\n\n      await WIKI.models.users.query().patch({\n        password: newPassword,\n        mustChangePwd: false\n      }).findById(usr.id)\n\n      return new Promise((resolve, reject) => {\n        context.req.logIn(usr, { session: false }, async err => {\n          if (err) { return reject(err) }\n          const jwtToken = await WIKI.models.users.refreshToken(usr)\n          resolve({ jwt: jwtToken.token })\n        })\n      })\n    } else {\n      throw new WIKI.Error.UserNotFound()\n    }\n  }\n\n  /**\n   * Send a password reset request\n   */\n  static async loginForgotPassword ({ email }, context) {\n    const usr = await WIKI.models.users.query().where({\n      email,\n      providerKey: 'local'\n    }).first()\n    if (!usr) {\n      WIKI.logger.debug(`Password reset attempt on nonexistant local account ${email}: [DISCARDED]`)\n      return\n    } else if (!usr.isActive) {\n      WIKI.logger.debug(`Password reset attempt on disabled local account ${email}: [DISCARDED]`)\n      return\n    }\n    const resetToken = await WIKI.models.userKeys.generateToken({\n      userId: usr.id,\n      kind: 'resetPwd'\n    })\n\n    await WIKI.mail.send({\n      template: 'accountResetPwd',\n      to: email,\n      subject: `Password Reset Request`,\n      data: {\n        preheadertext: `A password reset was requested for ${WIKI.config.title}`,\n        title: `A password reset was requested for ${WIKI.config.title}`,\n        content: `Click the button below to reset your password. If you didn't request this password reset, simply discard this email.`,\n        buttonLink: `${WIKI.config.host}/login-reset/${resetToken}`,\n        buttonText: 'Reset Password'\n      },\n      text: `A password reset was requested for wiki ${WIKI.config.title}. Open the following link to proceed: ${WIKI.config.host}/login-reset/${resetToken}`\n    })\n  }\n\n  /**\n   * Create a new user\n   *\n   * @param {Object} param0 User Fields\n   */\n  static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {\n    // Input sanitization\n    email = _.toLower(email)\n\n    // Input validation\n    let validation = null\n    if (providerKey === 'local') {\n      validation = validate({\n        email,\n        passwordRaw,\n        name\n      }, {\n        email: {\n          email: true,\n          length: {\n            maximum: 255\n          }\n        },\n        passwordRaw: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6\n          }\n        },\n        name: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255\n          }\n        }\n      }, { format: 'flat' })\n    } else {\n      validation = validate({\n        email,\n        name\n      }, {\n        email: {\n          email: true,\n          length: {\n            maximum: 255\n          }\n        },\n        name: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255\n          }\n        }\n      }, { format: 'flat' })\n    }\n\n    if (validation && validation.length > 0) {\n      throw new WIKI.Error.InputInvalid(validation[0])\n    }\n\n    // Check if email already exists\n    const usr = await WIKI.models.users.query().findOne({ email, providerKey })\n    if (!usr) {\n      // Create the account\n      let newUsrData = {\n        providerKey,\n        email,\n        name,\n        locale: 'en',\n        defaultEditor: 'markdown',\n        tfaIsActive: false,\n        isSystem: false,\n        isActive: true,\n        isVerified: true,\n        mustChangePwd: false\n      }\n\n      if (providerKey === `local`) {\n        newUsrData.password = passwordRaw\n        newUsrData.mustChangePwd = (mustChangePassword === true)\n      }\n\n      const newUsr = await WIKI.models.users.query().insert(newUsrData)\n\n      // Assign to group(s)\n      if (groups.length > 0) {\n        await newUsr.$relatedQuery('groups').relate(groups)\n      }\n\n      if (sendWelcomeEmail) {\n        // Send welcome email\n        await WIKI.mail.send({\n          template: 'accountWelcome',\n          to: email,\n          subject: `Welcome to the wiki ${WIKI.config.title}`,\n          data: {\n            preheadertext: `You've been invited to the wiki ${WIKI.config.title}`,\n            title: `You've been invited to the wiki ${WIKI.config.title}`,\n            content: `Click the button below to access the wiki.`,\n            buttonLink: `${WIKI.config.host}/login`,\n            buttonText: 'Login'\n          },\n          text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`\n        })\n      }\n    } else {\n      throw new WIKI.Error.AuthAccountAlreadyExists()\n    }\n  }\n\n  /**\n   * Update an existing user\n   *\n   * @param {Object} param0 User ID and fields to update\n   */\n  static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone, dateFormat, appearance }) {\n    const usr = await WIKI.models.users.query().findById(id)\n    if (usr) {\n      let usrData = {}\n      if (!_.isEmpty(email) && email !== usr.email) {\n        const dupUsr = await WIKI.models.users.query().select('id').where({\n          email,\n          providerKey: usr.providerKey\n        }).first()\n        if (dupUsr) {\n          throw new WIKI.Error.AuthAccountAlreadyExists()\n        }\n        usrData.email = _.toLower(email)\n      }\n      if (!_.isEmpty(name) && name !== usr.name) {\n        usrData.name = _.trim(name)\n      }\n      if (!_.isEmpty(newPassword)) {\n        if (newPassword.length < 6) {\n          throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')\n        }\n        usrData.password = newPassword\n      }\n      if (_.isArray(groups)) {\n        const usrGroupsRaw = await usr.$relatedQuery('groups')\n        const usrGroups = _.map(usrGroupsRaw, 'id')\n        // Relate added groups\n        const addUsrGroups = _.difference(groups, usrGroups)\n        for (const grp of addUsrGroups) {\n          await usr.$relatedQuery('groups').relate(grp)\n        }\n        // Unrelate removed groups\n        const remUsrGroups = _.difference(usrGroups, groups)\n        for (const grp of remUsrGroups) {\n          await usr.$relatedQuery('groups').unrelate().where('groupId', grp)\n        }\n      }\n      if (!_.isEmpty(location) && location !== usr.location) {\n        usrData.location = _.trim(location)\n      }\n      if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {\n        usrData.jobTitle = _.trim(jobTitle)\n      }\n      if (!_.isEmpty(timezone) && timezone !== usr.timezone) {\n        usrData.timezone = timezone\n      }\n      if (!_.isNil(dateFormat) && dateFormat !== usr.dateFormat) {\n        usrData.dateFormat = dateFormat\n      }\n      if (!_.isNil(appearance) && appearance !== usr.appearance) {\n        usrData.appearance = appearance\n      }\n      await WIKI.models.users.query().patch(usrData).findById(id)\n    } else {\n      throw new WIKI.Error.UserNotFound()\n    }\n  }\n\n  /**\n   * Delete a User\n   *\n   * @param {*} id User ID\n   */\n  static async deleteUser (id, replaceId) {\n    const usr = await WIKI.models.users.query().findById(id)\n    if (usr) {\n      await WIKI.models.assets.query().patch({ authorId: replaceId }).where('authorId', id)\n      await WIKI.models.comments.query().patch({ authorId: replaceId }).where('authorId', id)\n      await WIKI.models.pageHistory.query().patch({ authorId: replaceId }).where('authorId', id)\n      await WIKI.models.pages.query().patch({ authorId: replaceId }).where('authorId', id)\n      await WIKI.models.pages.query().patch({ creatorId: replaceId }).where('creatorId', id)\n\n      await WIKI.models.userKeys.query().delete().where('userId', id)\n      await WIKI.models.users.query().deleteById(id)\n    } else {\n      throw new WIKI.Error.UserNotFound()\n    }\n  }\n\n  /**\n   * Register a new user (client-side registration)\n   *\n   * @param {Object} param0 User fields\n   * @param {Object} context GraphQL Context\n   */\n  static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {\n    const localStrg = await WIKI.models.authentication.getStrategy('local')\n    // Check if self-registration is enabled\n    if (localStrg.selfRegistration || bypassChecks) {\n      // Input sanitization\n      email = _.toLower(email)\n\n      // Input validation\n      const validation = validate({\n        email,\n        password,\n        name\n      }, {\n        email: {\n          email: true,\n          length: {\n            maximum: 255\n          }\n        },\n        password: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 6\n          }\n        },\n        name: {\n          presence: {\n            allowEmpty: false\n          },\n          length: {\n            minimum: 2,\n            maximum: 255\n          }\n        }\n      }, { format: 'flat' })\n      if (validation && validation.length > 0) {\n        throw new WIKI.Error.InputInvalid(validation[0])\n      }\n\n      // Check if email domain is whitelisted\n      if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {\n        const emailDomain = _.last(email.split('@'))\n        if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {\n          throw new WIKI.Error.AuthRegistrationDomainUnauthorized()\n        }\n      }\n      // Check if email already exists\n      const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })\n      if (!usr) {\n        // Create the account\n        const newUsr = await WIKI.models.users.query().insert({\n          provider: 'local',\n          email,\n          name,\n          password,\n          locale: 'en',\n          defaultEditor: 'markdown',\n          tfaIsActive: false,\n          isSystem: false,\n          isActive: true,\n          isVerified: false\n        })\n\n        // Assign to group(s)\n        if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {\n          await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)\n        }\n\n        if (verify) {\n          // Create verification token\n          const verificationToken = await WIKI.models.userKeys.generateToken({\n            kind: 'verify',\n            userId: newUsr.id\n          })\n\n          // Send verification email\n          await WIKI.mail.send({\n            template: 'accountVerify',\n            to: email,\n            subject: 'Verify your account',\n            data: {\n              preheadertext: 'Verify your account in order to gain access to the wiki.',\n              title: 'Verify your account',\n              content: 'Click the button below in order to verify your account and gain access to the wiki.',\n              buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,\n              buttonText: 'Verify'\n            },\n            text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`\n          })\n        }\n        return true\n      } else {\n        throw new WIKI.Error.AuthAccountAlreadyExists()\n      }\n    } else {\n      throw new WIKI.Error.AuthRegistrationDisabled()\n    }\n  }\n\n  /**\n   * Logout the current user\n   */\n  static async logout (context) {\n    if (!context.req.user || context.req.user.id === 2) {\n      return '/'\n    }\n    const usr = await WIKI.models.users.query().findById(context.req.user.id).select('providerKey')\n    const provider = _.find(WIKI.auth.strategies, ['key', usr.providerKey])\n    return provider.logout ? provider.logout(provider.config, context) : '/'\n  }\n\n  static async getGuestUser () {\n    const user = await WIKI.models.users.query().findById(2).withGraphJoined('groups').modifyGraph('groups', builder => {\n      builder.select('groups.id', 'permissions')\n    })\n    if (!user) {\n      WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')\n      process.exit(1)\n    }\n    user.permissions = user.getGlobalPermissions()\n    return user\n  }\n\n  static async getRootUser () {\n    let user = await WIKI.models.users.query().findById(1)\n    if (!user) {\n      WIKI.logger.error('CRITICAL ERROR: Root Administrator user is missing!')\n      process.exit(1)\n    }\n    user.permissions = ['manage:system']\n    return user\n  }\n\n  /**\n   * Add / Update User Avatar Data\n   */\n  static async updateUserAvatarData (userId, data) {\n    try {\n      WIKI.logger.debug(`Updating user ${userId} avatar data...`)\n      if (data.length > 1024 * 1024) {\n        throw new Error('Avatar image filesize is too large. 1MB max.')\n      }\n      const existing = await WIKI.models.knex('userAvatars').select('id').where('id', userId).first()\n      if (existing) {\n        await WIKI.models.knex('userAvatars').where({\n          id: userId\n        }).update({\n          data\n        })\n      } else {\n        await WIKI.models.knex('userAvatars').insert({\n          id: userId,\n          data\n        })\n      }\n    } catch (err) {\n      WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}: ${err.message}`)\n    }\n  }\n\n  static async getUserAvatarData (userId) {\n    try {\n      const usrData = await WIKI.models.knex('userAvatars').where('id', userId).first()\n      if (usrData) {\n        return usrData.data\n      } else {\n        return null\n      }\n    } catch (err) {\n      WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}`)\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/analytics/azureinsights/code.yml",
    "content": "head: |\n  <script type=\"text/javascript\">\n    var sdkInstance=\"appInsightsSDK\";window[sdkInstance]=\"appInsights\";var aiName=window[sdkInstance],aisdk=window[aiName]||function(e){\n      function n(e){t[e]=function(){var n=arguments;t.queue.push(function(){t[e].apply(t,n)})}}var t={config:e};t.initialize=!0;var i=document,a=window;setTimeout(function(){var n=i.createElement(\"script\");n.src=e.url||\"https://az416426.vo.msecnd.net/next/ai.2.min.js\",i.getElementsByTagName(\"script\")[0].parentNode.appendChild(n)});try{t.cookie=i.cookie}catch(e){}t.queue=[],t.version=2;for(var r=[\"Event\",\"PageView\",\"Exception\",\"Trace\",\"DependencyData\",\"Metric\",\"PageViewPerformance\"];r.length;)n(\"track\"+r.pop());n(\"startTrackPage\"),n(\"stopTrackPage\");var s=\"Track\"+r[0];if(n(\"start\"+s),n(\"stop\"+s),n(\"setAuthenticatedUserContext\"),n(\"clearAuthenticatedUserContext\"),n(\"flush\"),!(!0===e.disableExceptionTracking||e.extensionConfig&&e.extensionConfig.ApplicationInsightsAnalytics&&!0===e.extensionConfig.ApplicationInsightsAnalytics.disableExceptionTracking)){n(\"_\"+(r=\"onerror\"));var o=a[r];a[r]=function(e,n,i,a,s){var c=o&&o(e,n,i,a,s);return!0!==c&&t[\"_\"+r]({message:e,url:n,lineNumber:i,columnNumber:a,error:s}),c},e.autoExceptionInstrumented=!0}return t\n    }({\n      instrumentationKey:\"{{instrumentationKey}}\"\n    });\n\n    window[aiName]=aisdk,aisdk.queue&&0===aisdk.queue.length&&aisdk.trackPageView({});\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/azureinsights/definition.yml",
    "content": "key: azureinsights\ntitle: Azure Application Insights\ndescription: Application Insights is an extensible Application Performance Management (APM) service for web developers on multiple platforms.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/azure.svg\nwebsite: https://azure.microsoft.com/en-us/services/monitor/\nisAvailable: true\nprops:\n  instrumentationKey:\n    type: String\n    title: Instrumentation Key\n    hint: Found in the Azure Portal in your Application Insights resource panel\n    order: 1\n"
  },
  {
    "path": "server/modules/analytics/baidutongji/code.yml",
    "content": "head: |\n  <!-- Baidu Tongji -->\n  <script>\n    var _hmt = _hmt || [];\n    (function() {\n      var hm = document.createElement(\"script\");\n      hm.src = \"https://hm.baidu.com/hm.js?{{propertyTrackingId}}\";\n      var s = document.getElementsByTagName(\"script\")[0]; \n      s.parentNode.insertBefore(hm, s);\n    })();\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/baidutongji/definition.yml",
    "content": "key: baidutongji\ntitle: Baidu Tongji\ndescription: Baidu Tongji is a web analytics service offered by Baidu that tracks and reports website traffic.\nauthor: lawrenceching\nlogo: https://static.requarks.io/logo/baidu.svg\nwebsite: https://tongji.baidu.com\nisAvailable: true\nprops:\n  propertyTrackingId:\n    type: String\n    title: Property Tracking ID\n    hint: Unique Property ID (found at the end of the tracking URL, e.g. https://hm.baidu.com/hm.js?XXXXXXXXXXXX)\n    order: 1\n"
  },
  {
    "path": "server/modules/analytics/countly/code.yml",
    "content": "head: |\n  <script type='text/javascript'>\n  //some default pre init\n  var Countly = Countly || {};\n  Countly.q = Countly.q || [];\n\n  //provide countly initialization parameters\n  Countly.app_key = '{{appKey}}';\n  Countly.url = '{{serverUrl}}';\n\n  Countly.q.push(['track_sessions']);\n  Countly.q.push(['track_pageview']);\n  Countly.q.push(['track_clicks']);\n  Countly.q.push(['track_scrolls']);\n  Countly.q.push(['track_errors']);\n  Countly.q.push(['track_links']);\n\n  //load countly script asynchronously\n  (function() {\n    var cly = document.createElement('script'); cly.type = 'text/javascript';\n    cly.async = true;\n    //enter url of script here\n    cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/18.8.2/countly.min.js';\n    cly.onload = function(){Countly.init()};\n    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);\n  })();\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/countly/definition.yml",
    "content": "key: countly\ntitle: Countly\ndescription: Countly is the best analytics platform to understand and enhance customer journeys in web, desktop and mobile applications.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/countly.svg\nwebsite: https://count.ly/\nisAvailable: true\nprops:\n  appKey:\n    type: String\n    title: App Key\n    hint: The App Key found under Management > Applications\n    order: 1\n  serverUrl:\n    type: String\n    title: Server URL\n    hint: The Count.ly server to report to. e.g. https://us-example.count.ly\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/elasticapm/code.yml",
    "content": "head: |\n  <!-- Elastic APM RUM -->\n  <script>\n    ;(function(d, s, c) {\n      var j = d.createElement(s),\n        t = d.getElementsByTagName(s)[0]\n\n      j.src = 'https://unpkg.com/@elastic/apm-rum/dist/bundles/elastic-apm-rum.umd.min.js'\n      j.onload = function() {elasticApm.init(c)}\n      t.parentNode.insertBefore(j, t)\n    })(document, 'script', {serviceName: '{{serviceName}}', serverUrl: '{{serverUrl}}', environment: '{{environment}}'})\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/elasticapm/definition.yml",
    "content": "key: elasticapm\ntitle: Elasticsearch APM RUM\ndescription: Real User Monitoring captures user interaction with clients such as web browsers.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/elasticsearch-apm.svg\nwebsite: https://www.elastic.co/solutions/apm\nisAvailable: true\nprops:\n  serverUrl:\n    type: String\n    title: APM Server URL\n    hint: The full URL to your APM server, including the port\n    default: http://apm.example.com:8200\n    order: 1\n  serviceName:\n    type: String\n    title: Service Name\n    hint: The name of the client reported to APM\n    default: wiki-js\n    order: 2\n  environment:\n    type: String\n    title: Environment\n    hint: e.g. production/development/test\n    default: ''\n    order: 3\n"
  },
  {
    "path": "server/modules/analytics/fathom/code.yml",
    "content": "head: |\n  <!-- Fathom - simple website analytics - https://github.com/usefathom/fathom -->\n  <script>\n  (function(f, a, t, h, o, m){\n    a[h]=a[h]||function(){\n      (a[h].q=a[h].q||[]).push(arguments)\n    };\n    o=f.createElement('script'),\n    m=f.getElementsByTagName('script')[0];\n    o.async=1; o.src=t; o.id='fathom-script';\n    m.parentNode.insertBefore(o,m)\n  })(document, window, '{{host}}/tracker.js', 'fathom');\n  fathom('set', 'siteId', '{{siteId}}');\n  fathom('trackPageview');\n  </script>\n  <!-- / Fathom -->\n"
  },
  {
    "path": "server/modules/analytics/fathom/definition.yml",
    "content": "key: fathom\ntitle: Fathom\ndescription: Fathom Analytics provides simple, useful website stats without tracking or storing personal data of your users.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/fathom.svg\nwebsite: https://usefathom.com/\nisAvailable: true\nprops:\n  host:\n    type: String\n    title: Fathom Server Host\n    hint: The hostname / ip adress where Fathom is installed, without the trailing slash. e.g. https://fathom.example.com\n    order: 1\n  siteId:\n    type: String\n    title: Site ID\n    hint: The alphanumeric identifier of your site\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/fullstory/code.yml",
    "content": "head: |\n  <script>\n  window['_fs_debug'] = false;\n  window['_fs_host'] = 'fullstory.com';\n  window['_fs_org'] = '{{org}}';\n  window['_fs_namespace'] = 'FS';\n  (function(m,n,e,t,l,o,g,y){\n      if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window[\"_fs_namespace\"].');} return;}\n      g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[];\n      o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src='https://'+_fs_host+'/s/fs.js';\n      y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);\n      g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)};\n      g.shutdown=function(){g(\"rec\",!1)};g.restart=function(){g(\"rec\",!0)};\n      g.consent=function(a){g(\"consent\",!arguments.length||a)};\n      g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};\n      g.clearUserCookie=function(){};\n  })(window,document,window['_fs_namespace'],'script','user');\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/fullstory/definition.yml",
    "content": "key: fullstory\ntitle: FullStory\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/fullstory.svg\nwebsite: https://www.fullstory.com\nisAvailable: true\nprops:\n  org:\n    type: String\n    title: Organization ID\n    hint: A 5 alphanumeric identifier, e.g. XXXXX\n    order: 1\n"
  },
  {
    "path": "server/modules/analytics/google/code.yml",
    "content": "head: |\n  <!-- Global site tag (gtag.js) - Google Analytics -->\n  <script async src=\"https://www.googletagmanager.com/gtag/js?id={{propertyTrackingId}}\"></script>\n  <script>\n    window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n\n    gtag('config', '{{propertyTrackingId}}');\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/google/definition.yml",
    "content": "key: google\ntitle: Google Analytics\ndescription: Google Analytics is a web analytics service offered by Google that tracks and reports website traffic.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/google-analytics.svg\nwebsite: https://analytics.google.com/\nisAvailable: true\nprops:\n  propertyTrackingId:\n    type: String\n    title: Property Tracking ID\n    hint: G-XXXXXXXXXX\n    order: 1\n"
  },
  {
    "path": "server/modules/analytics/gtm/code.yml",
    "content": "head: |\n  <!-- Google Tag Manager -->\n  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\n  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\n  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\n  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\n  })(window,document,'script','dataLayer','{{containerTrackingId}}');</script>\n  <!-- End Google Tag Manager -->\nbodyStart: |\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id={{containerTrackingId}}\"\n  height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n"
  },
  {
    "path": "server/modules/analytics/gtm/definition.yml",
    "content": "key: gtm\ntitle: Google Tag Manager\ndescription: Google Tag Manager is a tag management system created by Google to manage JavaScript and HTML tags used for tracking and analytics on websites.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/google-tag-manager.svg\nwebsite: https://tagmanager.google.com\nisAvailable: true\nprops:\n  containerTrackingId:\n    type: String\n    title: Container Tracking ID\n    hint: GTM-XXXXXXX\n    order: 1\n"
  },
  {
    "path": "server/modules/analytics/hotjar/code.yml",
    "content": "head: |\n  <!-- Hotjar Tracking Code -->\n  <script>\n    (function(h,o,t,j,a,r){\n      h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};\n      h._hjSettings={hjid:{{siteId}},hjsv:6};\n      a=o.getElementsByTagName('head')[0];\n      r=o.createElement('script');r.async=1;\n      r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;\n      a.appendChild(r);\n    })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/hotjar/definition.yml",
    "content": "key: hotjar\ntitle: Hotjar\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/hotjar.svg\nwebsite: https://www.hotjar.com\nisAvailable: true\nprops:\n  siteId:\n    type: String\n    title: Site ID\n    hint: A numeric identifier of your site\n    order: 1\n"
  },
  {
    "path": "server/modules/analytics/matomo/code.yml",
    "content": "head: |\n  <!-- Matomo -->\n  <script type=\"text/javascript\">\n    var _paq = window._paq = window._paq || [];\n    /* tracker methods like \"setCustomDimension\" should be called before \"trackPageView\" */\n    _paq.push(['trackPageView']);\n    _paq.push(['enableLinkTracking']);\n    (function() {\n      var u=\"{{serverHost}}/\";\n      _paq.push(['setTrackerUrl', u+'matomo.php']);\n      _paq.push(['setSiteId', '{{siteId}}']);\n      var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];\n      g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);\n    })();\n  </script>\n  <noscript><p><img src=\"{{serverHost}}/matomo.php?idsite={{siteId}}&amp;rec=1\" style=\"border:0;\" alt=\"\" /></p></noscript>\n  <!-- End Matomo Code -->\n"
  },
  {
    "path": "server/modules/analytics/matomo/definition.yml",
    "content": "key: matomo\ntitle: Matomo\ndescription: Take back control with Matomo Analytics – a powerful web analytics platform that gives you and your business 100% data ownership and user privacy protection.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/matomo.svg\nwebsite: https://matomo.org/\nisAvailable: true\nprops:\n  siteId:\n    type: String\n    title: Site ID\n    hint: The number index representing your site ID\n    default: 1\n    order: 1\n  serverHost:\n    type: String\n    title: Server Host\n    hint: Including https:// and optionally the port. Without trailing slash. (e.g. https://example.matomo.cloud)\n    default: https://example.matomo.cloud\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/newrelic/code.yml",
    "content": "head: |\n  <script type=\"text/javascript\">\n  window.NREUM||(NREUM={}),__nr_require=function(t,e,n){function r(n){if(!e[n]){var o=e[n]={exports:{}};t[n][0].call(o.exports,function(e){var o=t[n][1][e];return r(o||e)},o,o.exports)}return e[n].exports}if(\"function\"==typeof __nr_require)return __nr_require;for(var o=0;o<n.length;o++)r(n[o]);return r}({1:[function(t,e,n){function r(t){try{s.console&&console.log(t)}catch(e){}}var o,i=t(\"ee\"),a=t(23),s={};try{o=localStorage.getItem(\"__nr_flags\").split(\",\"),console&&\"function\"==typeof console.log&&(s.console=!0,o.indexOf(\"dev\")!==-1&&(s.dev=!0),o.indexOf(\"nr_dev\")!==-1&&(s.nrDev=!0))}catch(c){}s.nrDev&&i.on(\"internal-error\",function(t){r(t.stack)}),s.dev&&i.on(\"fn-err\",function(t,e,n){r(n.stack)}),s.dev&&(r(\"NR AGENT IN DEVELOPMENT MODE\"),r(\"flags: \"+a(s,function(t,e){return t}).join(\", \")))},{}],2:[function(t,e,n){function r(t,e,n,r,s){try{p?p-=1:o(s||new UncaughtException(t,e,n),!0)}catch(f){try{i(\"ierr\",[f,c.now(),!0])}catch(d){}}return\"function\"==typeof u&&u.apply(this,a(arguments))}function UncaughtException(t,e,n){this.message=t||\"Uncaught error with no additional information\",this.sourceURL=e,this.line=n}function o(t,e){var n=e?null:c.now();i(\"err\",[t,n])}var i=t(\"handle\"),a=t(24),s=t(\"ee\"),c=t(\"loader\"),f=t(\"gos\"),u=window.onerror,d=!1,l=\"nr@seenError\",p=0;c.features.err=!0,t(1),window.onerror=r;try{throw new Error}catch(h){\"stack\"in h&&(t(13),t(12),\"addEventListener\"in window&&t(6),c.xhrWrappable&&t(14),d=!0)}s.on(\"fn-start\",function(t,e,n){d&&(p+=1)}),s.on(\"fn-err\",function(t,e,n){d&&!n[l]&&(f(n,l,function(){return!0}),this.thrown=!0,o(n))}),s.on(\"fn-end\",function(){d&&!this.thrown&&p>0&&(p-=1)}),s.on(\"internal-error\",function(t){i(\"ierr\",[t,c.now(),!0])})},{}],3:[function(t,e,n){t(\"loader\").features.ins=!0},{}],4:[function(t,e,n){function r(){M++,j=y.hash,this[u]=x.now()}function o(){M--,y.hash!==j&&i(0,!0);var t=x.now();this[h]=~~this[h]+t-this[u],this[d]=t}function i(t,e){E.emit(\"newURL\",[\"\"+y,e])}function a(t,e){t.on(e,function(){this[e]=x.now()})}var s=\"-start\",c=\"-end\",f=\"-body\",u=\"fn\"+s,d=\"fn\"+c,l=\"cb\"+s,p=\"cb\"+c,h=\"jsTime\",m=\"fetch\",v=\"addEventListener\",w=window,y=w.location,x=t(\"loader\");if(w[v]&&x.xhrWrappable){var g=t(10),b=t(11),E=t(8),R=t(6),O=t(13),C=t(7),P=t(14),T=t(9),L=t(\"ee\"),S=L.get(\"tracer\");t(16),x.features.spa=!0;var j,M=0;L.on(u,r),L.on(l,r),L.on(d,o),L.on(p,o),L.buffer([u,d,\"xhr-done\",\"xhr-resolved\"]),R.buffer([u]),O.buffer([\"setTimeout\"+c,\"clearTimeout\"+s,u]),P.buffer([u,\"new-xhr\",\"send-xhr\"+s]),C.buffer([m+s,m+\"-done\",m+f+s,m+f+c]),E.buffer([\"newURL\"]),g.buffer([u]),b.buffer([\"propagate\",l,p,\"executor-err\",\"resolve\"+s]),S.buffer([u,\"no-\"+u]),T.buffer([\"new-jsonp\",\"cb-start\",\"jsonp-error\",\"jsonp-end\"]),a(P,\"send-xhr\"+s),a(L,\"xhr-resolved\"),a(L,\"xhr-done\"),a(C,m+s),a(C,m+\"-done\"),a(T,\"new-jsonp\"),a(T,\"jsonp-end\"),a(T,\"cb-start\"),E.on(\"pushState-end\",i),E.on(\"replaceState-end\",i),w[v](\"hashchange\",i,!0),w[v](\"load\",i,!0),w[v](\"popstate\",function(){i(0,M>1)},!0)}},{}],5:[function(t,e,n){function r(t){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var o=t(\"ee\"),i=t(\"handle\"),a=t(13),s=t(12),c=\"learResourceTimings\",f=\"addEventListener\",u=\"resourcetimingbufferfull\",d=\"bstResource\",l=\"resource\",p=\"-start\",h=\"-end\",m=\"fn\"+p,v=\"fn\"+h,w=\"bstTimer\",y=\"pushState\",x=t(\"loader\");x.features.stn=!0,t(8);var g=NREUM.o.EV;o.on(m,function(t,e){var n=t[0];n instanceof g&&(this.bstStart=x.now())}),o.on(v,function(t,e){var n=t[0];n instanceof g&&i(\"bst\",[n,e,this.bstStart,x.now()])}),a.on(m,function(t,e,n){this.bstStart=x.now(),this.bstType=n}),a.on(v,function(t,e){i(w,[e,this.bstStart,x.now(),this.bstType])}),s.on(m,function(){this.bstStart=x.now()}),s.on(v,function(t,e){i(w,[e,this.bstStart,x.now(),\"requestAnimationFrame\"])}),o.on(y+p,function(t){this.time=x.now(),this.startPath=location.pathname+location.hash}),o.on(y+h,function(t){i(\"bstHist\",[location.pathname+location.hash,this.startPath,this.time])}),f in window.performance&&(window.performance[\"c\"+c]?window.performance[f](u,function(t){i(d,[window.performance.getEntriesByType(l)]),window.performance[\"c\"+c]()},!1):window.performance[f](\"webkit\"+u,function(t){i(d,[window.performance.getEntriesByType(l)]),window.performance[\"webkitC\"+c]()},!1)),document[f](\"scroll\",r,{passive:!0}),document[f](\"keypress\",r,!1),document[f](\"click\",r,!1)}},{}],6:[function(t,e,n){function r(t){for(var e=t;e&&!e.hasOwnProperty(u);)e=Object.getPrototypeOf(e);e&&o(e)}function o(t){s.inPlace(t,[u,d],\"-\",i)}function i(t,e){return t[1]}var a=t(\"ee\").get(\"events\"),s=t(26)(a,!0),c=t(\"gos\"),f=XMLHttpRequest,u=\"addEventListener\",d=\"removeEventListener\";e.exports=a,\"getPrototypeOf\"in Object?(r(document),r(window),r(f.prototype)):f.prototype.hasOwnProperty(u)&&(o(window),o(f.prototype)),a.on(u+\"-start\",function(t,e){var n=t[1],r=c(n,\"nr@wrapped\",function(){function t(){if(\"function\"==typeof n.handleEvent)return n.handleEvent.apply(n,arguments)}var e={object:t,\"function\":n}[typeof n];return e?s(e,\"fn-\",null,e.name||\"anonymous\"):n});this.wrapped=t[1]=r}),a.on(d+\"-start\",function(t){t[1]=this.wrapped||t[1]})},{}],7:[function(t,e,n){function r(t,e,n){var r=t[e];\"function\"==typeof r&&(t[e]=function(){var t=r.apply(this,arguments);return o.emit(n+\"start\",arguments,t),t.then(function(e){return o.emit(n+\"end\",[null,e],t),e},function(e){throw o.emit(n+\"end\",[e],t),e})})}var o=t(\"ee\").get(\"fetch\"),i=t(23);e.exports=o;var a=window,s=\"fetch-\",c=s+\"body-\",f=[\"arrayBuffer\",\"blob\",\"json\",\"text\",\"formData\"],u=a.Request,d=a.Response,l=a.fetch,p=\"prototype\";u&&d&&l&&(i(f,function(t,e){r(u[p],e,c),r(d[p],e,c)}),r(a,\"fetch\",s),o.on(s+\"end\",function(t,e){var n=this;if(e){var r=e.headers.get(\"content-length\");null!==r&&(n.rxSize=r),o.emit(s+\"done\",[null,e],n)}else o.emit(s+\"done\",[t],n)}))},{}],8:[function(t,e,n){var r=t(\"ee\").get(\"history\"),o=t(26)(r);e.exports=r,o.inPlace(window.history,[\"pushState\",\"replaceState\"],\"-\")},{}],9:[function(t,e,n){function r(t){function e(){c.emit(\"jsonp-end\",[],l),t.removeEventListener(\"load\",e,!1),t.removeEventListener(\"error\",n,!1)}function n(){c.emit(\"jsonp-error\",[],l),c.emit(\"jsonp-end\",[],l),t.removeEventListener(\"load\",e,!1),t.removeEventListener(\"error\",n,!1)}var r=t&&\"string\"==typeof t.nodeName&&\"script\"===t.nodeName.toLowerCase();if(r){var o=\"function\"==typeof t.addEventListener;if(o){var a=i(t.src);if(a){var u=s(a),d=\"function\"==typeof u.parent[u.key];if(d){var l={};f.inPlace(u.parent,[u.key],\"cb-\",l),t.addEventListener(\"load\",e,!1),t.addEventListener(\"error\",n,!1),c.emit(\"new-jsonp\",[t.src],l)}}}}}function o(){return\"addEventListener\"in window}function i(t){var e=t.match(u);return e?e[1]:null}function a(t,e){var n=t.match(l),r=n[1],o=n[3];return o?a(o,e[r]):e[r]}function s(t){var e=t.match(d);return e&&e.length>=3?{key:e[2],parent:a(e[1],window)}:{key:t,parent:window}}var c=t(\"ee\").get(\"jsonp\"),f=t(26)(c);if(e.exports=c,o()){var u=/[?&](?:callback|cb)=([^&#]+)/,d=/(.*)\\.([^.]+)/,l=/^(\\w+)(\\.|$)(.*)$/,p=[\"appendChild\",\"insertBefore\",\"replaceChild\"];f.inPlace(HTMLElement.prototype,p,\"dom-\"),f.inPlace(HTMLHeadElement.prototype,p,\"dom-\"),f.inPlace(HTMLBodyElement.prototype,p,\"dom-\"),c.on(\"dom-start\",function(t){r(t[0])})}},{}],10:[function(t,e,n){var r=t(\"ee\").get(\"mutation\"),o=t(26)(r),i=NREUM.o.MO;e.exports=r,i&&(window.MutationObserver=function(t){return this instanceof i?new i(o(t,\"fn-\")):i.apply(this,arguments)},MutationObserver.prototype=i.prototype)},{}],11:[function(t,e,n){function r(t){var e=a.context(),n=s(t,\"executor-\",e),r=new f(n);return a.context(r).getCtx=function(){return e},a.emit(\"new-promise\",[r,e],e),r}function o(t,e){return e}var i=t(26),a=t(\"ee\").get(\"promise\"),s=i(a),c=t(23),f=NREUM.o.PR;e.exports=a,f&&(window.Promise=r,[\"all\",\"race\"].forEach(function(t){var e=f[t];f[t]=function(n){function r(t){return function(){a.emit(\"propagate\",[null,!o],i),o=o||!t}}var o=!1;c(n,function(e,n){Promise.resolve(n).then(r(\"all\"===t),r(!1))});var i=e.apply(f,arguments),s=f.resolve(i);return s}}),[\"resolve\",\"reject\"].forEach(function(t){var e=f[t];f[t]=function(t){var n=e.apply(f,arguments);return t!==n&&a.emit(\"propagate\",[t,!0],n),n}}),f.prototype[\"catch\"]=function(t){return this.then(null,t)},f.prototype=Object.create(f.prototype,{constructor:{value:r}}),c(Object.getOwnPropertyNames(f),function(t,e){try{r[e]=f[e]}catch(n){}}),a.on(\"executor-start\",function(t){t[0]=s(t[0],\"resolve-\",this),t[1]=s(t[1],\"resolve-\",this)}),a.on(\"executor-err\",function(t,e,n){t[1](n)}),s.inPlace(f.prototype,[\"then\"],\"then-\",o),a.on(\"then-start\",function(t,e){this.promise=e,t[0]=s(t[0],\"cb-\",this),t[1]=s(t[1],\"cb-\",this)}),a.on(\"then-end\",function(t,e,n){this.nextPromise=n;var r=this.promise;a.emit(\"propagate\",[r,!0],n)}),a.on(\"cb-end\",function(t,e,n){a.emit(\"propagate\",[n,!0],this.nextPromise)}),a.on(\"propagate\",function(t,e,n){this.getCtx&&!e||(this.getCtx=function(){if(t instanceof Promise)var e=a.context(t);return e&&e.getCtx?e.getCtx():this})}),r.toString=function(){return\"\"+f})},{}],12:[function(t,e,n){var r=t(\"ee\").get(\"raf\"),o=t(26)(r),i=\"equestAnimationFrame\";e.exports=r,o.inPlace(window,[\"r\"+i,\"mozR\"+i,\"webkitR\"+i,\"msR\"+i],\"raf-\"),r.on(\"raf-start\",function(t){t[0]=o(t[0],\"fn-\")})},{}],13:[function(t,e,n){function r(t,e,n){t[0]=a(t[0],\"fn-\",null,n)}function o(t,e,n){this.method=n,this.timerDuration=isNaN(t[1])?0:+t[1],t[0]=a(t[0],\"fn-\",this,n)}var i=t(\"ee\").get(\"timer\"),a=t(26)(i),s=\"setTimeout\",c=\"setInterval\",f=\"clearTimeout\",u=\"-start\",d=\"-\";e.exports=i,a.inPlace(window,[s,\"setImmediate\"],s+d),a.inPlace(window,[c],c+d),a.inPlace(window,[f,\"clearImmediate\"],f+d),i.on(c+u,r),i.on(s+u,o)},{}],14:[function(t,e,n){function r(t,e){d.inPlace(e,[\"onreadystatechange\"],\"fn-\",s)}function o(){var t=this,e=u.context(t);t.readyState>3&&!e.resolved&&(e.resolved=!0,u.emit(\"xhr-resolved\",[],t)),d.inPlace(t,y,\"fn-\",s)}function i(t){x.push(t),h&&(b?b.then(a):v?v(a):(E=-E,R.data=E))}function a(){for(var t=0;t<x.length;t++)r([],x[t]);x.length&&(x=[])}function s(t,e){return e}function c(t,e){for(var n in t)e[n]=t[n];return e}t(6);var f=t(\"ee\"),u=f.get(\"xhr\"),d=t(26)(u),l=NREUM.o,p=l.XHR,h=l.MO,m=l.PR,v=l.SI,w=\"readystatechange\",y=[\"onload\",\"onerror\",\"onabort\",\"onloadstart\",\"onloadend\",\"onprogress\",\"ontimeout\"],x=[];e.exports=u;var g=window.XMLHttpRequest=function(t){var e=new p(t);try{u.emit(\"new-xhr\",[e],e),e.addEventListener(w,o,!1)}catch(n){try{u.emit(\"internal-error\",[n])}catch(r){}}return e};if(c(p,g),g.prototype=p.prototype,d.inPlace(g.prototype,[\"open\",\"send\"],\"-xhr-\",s),u.on(\"send-xhr-start\",function(t,e){r(t,e),i(e)}),u.on(\"open-xhr-start\",r),h){var b=m&&m.resolve();if(!v&&!m){var E=1,R=document.createTextNode(E);new h(a).observe(R,{characterData:!0})}}else f.on(\"fn-end\",function(t){t[0]&&t[0].type===w||a()})},{}],15:[function(t,e,n){function r(){var t=window.NREUM,e=t.info.accountID||null,n=t.info.agentID||null,r=t.info.trustKey||null,i=\"btoa\"in window&&\"function\"==typeof window.btoa;if(!e||!n||!i)return null;var a={v:[0,1],d:{ty:\"Browser\",ac:e,ap:n,id:o.generateCatId(),tr:o.generateCatId(),ti:Date.now()}};return r&&e!==r&&(a.d.tk=r),btoa(JSON.stringify(a))}var o=t(21);e.exports={generateTraceHeader:r}},{}],16:[function(t,e,n){function r(t){var e=this.params,n=this.metrics;if(!this.ended){this.ended=!0;for(var r=0;r<p;r++)t.removeEventListener(l[r],this.listener,!1);e.aborted||(n.duration=s.now()-this.startTime,this.loadCaptureCalled||4!==t.readyState?null==e.status&&(e.status=0):a(this,t),n.cbTime=this.cbTime,d.emit(\"xhr-done\",[t],t),c(\"xhr\",[e,n,this.startTime]))}}function o(t,e){var n=t.responseType;if(\"json\"===n&&null!==e)return e;var r=\"arraybuffer\"===n||\"blob\"===n||\"json\"===n?t.response:t.responseText;return v(r)}function i(t,e){var n=f(e),r=t.params;r.host=n.hostname+\":\"+n.port,r.pathname=n.pathname,t.sameOrigin=n.sameOrigin}function a(t,e){t.params.status=e.status;var n=o(e,t.lastSize);if(n&&(t.metrics.rxSize=n),t.sameOrigin){var r=e.getResponseHeader(\"X-NewRelic-App-Data\");r&&(t.params.cat=r.split(\", \").pop())}t.loadCaptureCalled=!0}var s=t(\"loader\");if(s.xhrWrappable){var c=t(\"handle\"),f=t(17),u=t(15).generateTraceHeader,d=t(\"ee\"),l=[\"load\",\"error\",\"abort\",\"timeout\"],p=l.length,h=t(\"id\"),m=t(20),v=t(19),w=window.XMLHttpRequest;s.features.xhr=!0,t(14),d.on(\"new-xhr\",function(t){var e=this;e.totalCbs=0,e.called=0,e.cbTime=0,e.end=r,e.ended=!1,e.xhrGuids={},e.lastSize=null,e.loadCaptureCalled=!1,t.addEventListener(\"load\",function(n){a(e,t)},!1),m&&(m>34||m<10)||window.opera||t.addEventListener(\"progress\",function(t){e.lastSize=t.loaded},!1)}),d.on(\"open-xhr-start\",function(t){this.params={method:t[0]},i(this,t[1]),this.metrics={}}),d.on(\"open-xhr-end\",function(t,e){\"loader_config\"in NREUM&&\"xpid\"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader(\"X-NewRelic-ID\",NREUM.loader_config.xpid);var n=!1;if(\"init\"in NREUM&&\"distributed_tracing\"in NREUM.init&&(n=!!NREUM.init.distributed_tracing.enabled),n&&this.sameOrigin){var r=u();r&&e.setRequestHeader(\"newrelic\",r)}}),d.on(\"send-xhr-start\",function(t,e){var n=this.metrics,r=t[0],o=this;if(n&&r){var i=v(r);i&&(n.txSize=i)}this.startTime=s.now(),this.listener=function(t){try{\"abort\"!==t.type||o.loadCaptureCalled||(o.params.aborted=!0),(\"load\"!==t.type||o.called===o.totalCbs&&(o.onloadCalled||\"function\"!=typeof e.onload))&&o.end(e)}catch(n){try{d.emit(\"internal-error\",[n])}catch(r){}}};for(var a=0;a<p;a++)e.addEventListener(l[a],this.listener,!1)}),d.on(\"xhr-cb-time\",function(t,e,n){this.cbTime+=t,e?this.onloadCalled=!0:this.called+=1,this.called!==this.totalCbs||!this.onloadCalled&&\"function\"==typeof n.onload||this.end(n)}),d.on(\"xhr-load-added\",function(t,e){var n=\"\"+h(t)+!!e;this.xhrGuids&&!this.xhrGuids[n]&&(this.xhrGuids[n]=!0,this.totalCbs+=1)}),d.on(\"xhr-load-removed\",function(t,e){var n=\"\"+h(t)+!!e;this.xhrGuids&&this.xhrGuids[n]&&(delete this.xhrGuids[n],this.totalCbs-=1)}),d.on(\"addEventListener-end\",function(t,e){e instanceof w&&\"load\"===t[0]&&d.emit(\"xhr-load-added\",[t[1],t[2]],e)}),d.on(\"removeEventListener-end\",function(t,e){e instanceof w&&\"load\"===t[0]&&d.emit(\"xhr-load-removed\",[t[1],t[2]],e)}),d.on(\"fn-start\",function(t,e,n){e instanceof w&&(\"onload\"===n&&(this.onload=!0),(\"load\"===(t[0]&&t[0].type)||this.onload)&&(this.xhrCbStart=s.now()))}),d.on(\"fn-end\",function(t,e){this.xhrCbStart&&d.emit(\"xhr-cb-time\",[s.now()-this.xhrCbStart,this.onload,e],e)})}},{}],17:[function(t,e,n){e.exports=function(t){var e=document.createElement(\"a\"),n=window.location,r={};e.href=t,r.port=e.port;var o=e.href.split(\"://\");!r.port&&o[1]&&(r.port=o[1].split(\"/\")[0].split(\"@\").pop().split(\":\")[1]),r.port&&\"0\"!==r.port||(r.port=\"https\"===o[0]?\"443\":\"80\"),r.hostname=e.hostname||n.hostname,r.pathname=e.pathname,r.protocol=o[0],\"/\"!==r.pathname.charAt(0)&&(r.pathname=\"/\"+r.pathname);var i=!e.protocol||\":\"===e.protocol||e.protocol===n.protocol,a=e.hostname===document.domain&&e.port===n.port;return r.sameOrigin=i&&(!e.hostname||a),r}},{}],18:[function(t,e,n){function r(){}function o(t,e,n){return function(){return i(t,[f.now()].concat(s(arguments)),e?null:this,n),e?void 0:this}}var i=t(\"handle\"),a=t(23),s=t(24),c=t(\"ee\").get(\"tracer\"),f=t(\"loader\"),u=NREUM;\"undefined\"==typeof window.newrelic&&(newrelic=u);var d=[\"setPageViewName\",\"setCustomAttribute\",\"setErrorHandler\",\"finished\",\"addToTrace\",\"inlineHit\",\"addRelease\"],l=\"api-\",p=l+\"ixn-\";a(d,function(t,e){u[e]=o(l+e,!0,\"api\")}),u.addPageAction=o(l+\"addPageAction\",!0),u.setCurrentRouteName=o(l+\"routeName\",!0),e.exports=newrelic,u.interaction=function(){return(new r).get()};var h=r.prototype={createTracer:function(t,e){var n={},r=this,o=\"function\"==typeof e;return i(p+\"tracer\",[f.now(),t,n],r),function(){if(c.emit((o?\"\":\"no-\")+\"fn-start\",[f.now(),r,o],n),o)try{return e.apply(this,arguments)}catch(t){throw c.emit(\"fn-err\",[arguments,this,t],n),t}finally{c.emit(\"fn-end\",[f.now()],n)}}}};a(\"actionText,setName,setAttribute,save,ignore,onEnd,getContext,end,get\".split(\",\"),function(t,e){h[e]=o(p+e)}),newrelic.noticeError=function(t,e){\"string\"==typeof t&&(t=new Error(t)),i(\"err\",[t,f.now(),!1,e])}},{}],19:[function(t,e,n){e.exports=function(t){if(\"string\"==typeof t&&t.length)return t.length;if(\"object\"==typeof t){if(\"undefined\"!=typeof ArrayBuffer&&t instanceof ArrayBuffer&&t.byteLength)return t.byteLength;if(\"undefined\"!=typeof Blob&&t instanceof Blob&&t.size)return t.size;if(!(\"undefined\"!=typeof FormData&&t instanceof FormData))try{return JSON.stringify(t).length}catch(e){return}}}},{}],20:[function(t,e,n){var r=0,o=navigator.userAgent.match(/Firefox[\\/\\s](\\d+\\.\\d+)/);o&&(r=+o[1]),e.exports=r},{}],21:[function(t,e,n){function r(){function t(){return e?15&e[n++]:16*Math.random()|0}var e=null,n=0,r=window.crypto||window.msCrypto;r&&r.getRandomValues&&(e=r.getRandomValues(new Uint8Array(31)));for(var o,i=\"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\",a=\"\",s=0;s<i.length;s++)o=i[s],\"x\"===o?a+=t().toString(16):\"y\"===o?(o=3&t()|8,a+=o.toString(16)):a+=o;return a}function o(){function t(){return e?15&e[n++]:16*Math.random()|0}var e=null,n=0,r=window.crypto||window.msCrypto;r&&r.getRandomValues&&Uint8Array&&(e=r.getRandomValues(new Uint8Array(31)));for(var o=[],i=0;i<16;i++)o.push(t().toString(16));return o.join(\"\")}e.exports={generateUuid:r,generateCatId:o}},{}],22:[function(t,e,n){function r(t,e){if(!o)return!1;if(t!==o)return!1;if(!e)return!0;if(!i)return!1;for(var n=i.split(\".\"),r=e.split(\".\"),a=0;a<r.length;a++)if(r[a]!==n[a])return!1;return!0}var o=null,i=null,a=/Version\\/(\\S+)\\s+Safari/;if(navigator.userAgent){var s=navigator.userAgent,c=s.match(a);c&&s.indexOf(\"Chrome\")===-1&&s.indexOf(\"Chromium\")===-1&&(o=\"Safari\",i=c[1])}e.exports={agent:o,version:i,match:r}},{}],23:[function(t,e,n){function r(t,e){var n=[],r=\"\",i=0;for(r in t)o.call(t,r)&&(n[i]=e(r,t[r]),i+=1);return n}var o=Object.prototype.hasOwnProperty;e.exports=r},{}],24:[function(t,e,n){function r(t,e,n){e||(e=0),\"undefined\"==typeof n&&(n=t?t.length:0);for(var r=-1,o=n-e||0,i=Array(o<0?0:o);++r<o;)i[r]=t[e+r];return i}e.exports=r},{}],25:[function(t,e,n){e.exports={exists:\"undefined\"!=typeof window.performance&&window.performance.timing&&\"undefined\"!=typeof window.performance.timing.navigationStart}},{}],26:[function(t,e,n){function r(t){return!(t&&t instanceof Function&&t.apply&&!t[a])}var o=t(\"ee\"),i=t(24),a=\"nr@original\",s=Object.prototype.hasOwnProperty,c=!1;e.exports=function(t,e){function n(t,e,n,o){function nrWrapper(){var r,a,s,c;try{a=this,r=i(arguments),s=\"function\"==typeof n?n(r,a):n||{}}catch(f){l([f,\"\",[r,a,o],s])}u(e+\"start\",[r,a,o],s);try{return c=t.apply(a,r)}catch(d){throw u(e+\"err\",[r,a,d],s),d}finally{u(e+\"end\",[r,a,c],s)}}return r(t)?t:(e||(e=\"\"),nrWrapper[a]=t,d(t,nrWrapper),nrWrapper)}function f(t,e,o,i){o||(o=\"\");var a,s,c,f=\"-\"===o.charAt(0);for(c=0;c<e.length;c++)s=e[c],a=t[s],r(a)||(t[s]=n(a,f?s+o:o,i,s))}function u(n,r,o){if(!c||e){var i=c;c=!0;try{t.emit(n,r,o,e)}catch(a){l([a,n,r,o])}c=i}}function d(t,e){if(Object.defineProperty&&Object.keys)try{var n=Object.keys(t);return n.forEach(function(n){Object.defineProperty(e,n,{get:function(){return t[n]},set:function(e){return t[n]=e,e}})}),e}catch(r){l([r])}for(var o in t)s.call(t,o)&&(e[o]=t[o]);return e}function l(e){try{t.emit(\"internal-error\",e)}catch(n){}}return t||(t=o),n.inPlace=f,n.flag=a,n}},{}],ee:[function(t,e,n){function r(){}function o(t){function e(t){return t&&t instanceof r?t:t?c(t,s,i):i()}function n(n,r,o,i){if(!l.aborted||i){t&&t(n,r,o);for(var a=e(o),s=m(n),c=s.length,f=0;f<c;f++)s[f].apply(a,r);var d=u[x[n]];return d&&d.push([g,n,r,a]),a}}function p(t,e){y[t]=m(t).concat(e)}function h(t,e){var n=y[t];if(n)for(var r=0;r<n.length;r++)n[r]===e&&n.splice(r,1)}function m(t){return y[t]||[]}function v(t){return d[t]=d[t]||o(n)}function w(t,e){f(t,function(t,n){e=e||\"feature\",x[n]=e,e in u||(u[e]=[])})}var y={},x={},g={on:p,addEventListener:p,removeEventListener:h,emit:n,get:v,listeners:m,context:e,buffer:w,abort:a,aborted:!1};return g}function i(){return new r}function a(){(u.api||u.feature)&&(l.aborted=!0,u=l.backlog={})}var s=\"nr@context\",c=t(\"gos\"),f=t(23),u={},d={},l=e.exports=o();l.backlog=u},{}],gos:[function(t,e,n){function r(t,e,n){if(o.call(t,e))return t[e];var r=n();if(Object.defineProperty&&Object.keys)try{return Object.defineProperty(t,e,{value:r,writable:!0,enumerable:!1}),r}catch(i){}return t[e]=r,r}var o=Object.prototype.hasOwnProperty;e.exports=r},{}],handle:[function(t,e,n){function r(t,e,n,r){o.buffer([t],r),o.emit(t,e,n)}var o=t(\"ee\").get(\"handle\");e.exports=r,r.ee=o},{}],id:[function(t,e,n){function r(t){var e=typeof t;return!t||\"object\"!==e&&\"function\"!==e?-1:t===window?0:a(t,i,function(){return o++})}var o=1,i=\"nr@id\",a=t(\"gos\");e.exports=r},{}],loader:[function(t,e,n){function r(){if(!E++){var t=b.info=NREUM.info,e=p.getElementsByTagName(\"script\")[0];if(setTimeout(u.abort,3e4),!(t&&t.licenseKey&&t.applicationID&&e))return u.abort();f(x,function(e,n){t[e]||(t[e]=n)}),c(\"mark\",[\"onload\",a()+b.offset],null,\"api\");var n=p.createElement(\"script\");n.src=\"https://\"+t.agent,e.parentNode.insertBefore(n,e)}}function o(){\"complete\"===p.readyState&&i()}function i(){c(\"mark\",[\"domContent\",a()+b.offset],null,\"api\")}function a(){return R.exists&&performance.now?Math.round(performance.now()):(s=Math.max((new Date).getTime(),s))-b.offset}var s=(new Date).getTime(),c=t(\"handle\"),f=t(23),u=t(\"ee\"),d=t(22),l=window,p=l.document,h=\"addEventListener\",m=\"attachEvent\",v=l.XMLHttpRequest,w=v&&v.prototype;NREUM.o={ST:setTimeout,SI:l.setImmediate,CT:clearTimeout,XHR:v,REQ:l.Request,EV:l.Event,PR:l.Promise,MO:l.MutationObserver};var y=\"\"+location,x={beacon:\"bam.nr-data.net\",errorBeacon:\"bam.nr-data.net\",agent:\"js-agent.newrelic.com/nr-spa-1123.min.js\"},g=v&&w&&w[h]&&!/CriOS/.test(navigator.userAgent),b=e.exports={offset:s,now:a,origin:y,features:{},xhrWrappable:g,userAgent:d};t(18),p[h]?(p[h](\"DOMContentLoaded\",i,!1),l[h](\"load\",r,!1)):(p[m](\"onreadystatechange\",o),l[m](\"onload\",r)),c(\"mark\",[\"firstbyte\",s],null,\"api\");var E=0,R=t(25)},{}]},{},[\"loader\",2,16,5,3,4]);\n  ;NREUM.info={beacon:\"{{beacon}}\",errorBeacon:\"{{errorBeacon}}\",licenseKey:\"{{licenseKey}}\",applicationID:\"{{appId}}\",sa:1}\n  </script>\n"
  },
  {
    "path": "server/modules/analytics/newrelic/definition.yml",
    "content": "key: newrelic\ntitle: New Relic Browser\ndescription: New Relic Browser provides deep visibility and insight into how your users are interacting with your application or website.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/newrelic.svg\nwebsite: https://newrelic.com/products/browser-monitoring\nisAvailable: true\nprops:\n  licenseKey:\n    type: String\n    title: License Key\n    hint: Found at the very end of the code snippet provided by New Relic Browser\n    order: 1\n  appId:\n    type: String\n    title: Application ID\n    hint: Found at the very end of the code snippet provided by New Relic Browser\n    order: 2\n  beacon:\n    type: String\n    title: Beacon\n    default: bam.nr-data.net\n    hint: Found at the very end of the code snippet provided by New Relic Browser. Differs for US and EU servers.\n    order: 3\n  errorBeacon:\n    type: String\n    title: Error Beacon\n    default: bam.nr-data.net\n    hint: Found at the very end of the code snippet provided by New Relic Browser. Differs for US and EU servers.\n    order: 4\n"
  },
  {
    "path": "server/modules/analytics/plausible/code.yml",
    "content": "head: |\n  <script defer data-domain=\"{{domain}}\" src=\"{{plausibleJsSrc}}\"></script>\n"
  },
  {
    "path": "server/modules/analytics/plausible/definition.yml",
    "content": "key: plausible\ntitle: Plausible Analytics\ndescription: Simple, open-source, lightweight and privacy-friendly web analytics alternative to Google Analytics.\nauthor: requarks.io\nlogo: https://cdn.js.wiki/images/3rdparty/plausible.svg\nwebsite: https://plausible.io\nisAvailable: true\nprops:\n  domain:\n    type: String\n    title: Domain\n    hint: The value of the data-domain property\n    order: 1\n  plausibleJsSrc:\n    type: String\n    default: https://plausible.io/js/plausible.js\n    title: Plausible JS Script source\n    hint: The URL of Plausbile Script (only needed when using a self hosted installation)\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/statcounter/code.yml",
    "content": "head: |\n  <!-- Statcounter Code -->\n  <script type=\"text/javascript\">\n  var sc_project={{projectId}};\n  var sc_invisible=1;\n  var sc_security=\"{{securityToken}}\";\n  var sc_https=1;\n  var sc_remove_link=1;\n  </script>\n  <script type=\"text/javascript\" src=\"https://www.statcounter.com/counter/counter.js\" async></script>\n  <noscript><div class=\"statcounter\"><img class=\"statcounter\" src=\"https://c.statcounter.com/{{projectId}}/0/{{securityToken}}/1/\" alt=\"Web Analytics Made Easy - StatCounter\"></div></noscript>\n  <!-- End of Statcounter Code -->\n"
  },
  {
    "path": "server/modules/analytics/statcounter/definition.yml",
    "content": "key: statcounter\ntitle: StatCounter\ndescription: See how Statcounter's easy-to-use features give you everything you need to understand your visitors and increase your website traffic.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/statcountr.svg\nwebsite: https://statcounter.com/\nisAvailable: true\nprops:\n  projectId:\n    type: String\n    title: Project ID\n    hint: Unique Project ID, found in the code snippet provided by StatCounter\n    order: 1\n  securityToken:\n    type: String\n    title: Security Token\n    hint: Security token, found in the code snippet provided by StatCounter\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/umami/code.yml",
    "content": "head: |\n  <script async defer data-website-id=\"{{websiteID}}\" src=\"{{url}}/umami.js\"></script>\n"
  },
  {
    "path": "server/modules/analytics/umami/definition.yml",
    "content": "key: umami\ntitle: Umami Analytics v1\ndescription: Umami is a simple, fast, privacy-focused alternative to Google Analytics.\nauthor: CDN18\nlogo: https://static.requarks.io/logo/umami.svg\nwebsite: https://umami.is\nisAvailable: true\nprops:\n  websiteID:\n    type: String\n    title: Website ID\n    order: 1\n  url:\n    type: String\n    title: Umami Server URL\n    hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com)\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/umami2/code.yml",
    "content": "head: |\n  <script async src=\"{{url}}/script.js\" data-website-id=\"{{websiteID}}\"></script>\n"
  },
  {
    "path": "server/modules/analytics/umami2/definition.yml",
    "content": "key: umami2\ntitle: Umami Analytics v2\ndescription: Umami is a simple, fast, privacy-focused alternative to Google Analytics.\nauthor: CDN18\nlogo: https://static.requarks.io/logo/umami.svg\nwebsite: https://umami.is\nisAvailable: true\nprops:\n  websiteID:\n    type: String\n    title: Website ID\n    order: 1\n  url:\n    type: String\n    title: Umami Server URL\n    hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com)\n    order: 2\n"
  },
  {
    "path": "server/modules/analytics/yandex/code.yml",
    "content": "head: |\n  <!-- Yandex.Metrika counter -->\n  <script type=\"text/javascript\" >\n    (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};\n    m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})\n    (window, document, \"script\", \"https://mc.yandex.ru/metrika/tag.js\", \"ym\");\n\n    ym({{tagNumber}}, \"init\", {\n      clickmap:true,\n      trackLinks:true,\n      accurateTrackBounce:true,\n      webvisor:true\n    });\n  </script>\n  <noscript><div><img src=\"https://mc.yandex.ru/watch/{{tagNumber}}\" style=\"position:absolute; left:-9999px;\" alt=\"\" /></div></noscript>\n  <!-- /Yandex.Metrika counter -->\n"
  },
  {
    "path": "server/modules/analytics/yandex/definition.yml",
    "content": "key: yandex\ntitle: Yandex Metrica\ndescription: From traffic trends to mouse movements – get a comprehensive understanding of your online audience and drive business growth.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/yandex.svg\nwebsite: https://metrica.yandex.com\nisAvailable: true\nprops:\n  tagNumber:\n    type: String\n    title: Tag Number\n    hint: When creating the tag, select \"CMS and website builders\" and copy the provided Tag Number\n    order: 1\n"
  },
  {
    "path": "server/modules/authentication/auth0/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Auth0 Account\n// ------------------------------------\n\nconst Auth0Strategy = require('passport-auth0').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new Auth0Strategy({\n        domain: conf.domain,\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, extraParams, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  },\n  logout (conf) {\n    return `https://${conf.domain}/v2/logout?${new URLSearchParams({ client_id: conf.clientId, returnTo: WIKI.config.host }).toString()}`\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/auth0/definition.yml",
    "content": "key: auth0\ntitle: Auth0\ndescription: Auth0 provides universal identity platform for web, mobile, IoT, and internal applications.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/auth0.svg\ncolor: deep-orange\nwebsite: https://auth0.com/\nisAvailable: true\nuseForm: false\nscopes:\n  - openid\n  - profile\n  - email\nprops:\n  domain:\n    type: String\n    title: Domain\n    hint: Your Auth0 domain (e.g. something.auth0.com)\n    order: 1\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 2\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 3\n"
  },
  {
    "path": "server/modules/authentication/azure/authentication.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n// ------------------------------------\n// Azure AD Account\n// ------------------------------------\n\nconst OIDCStrategy = require('passport-azure-ad').OIDCStrategy\n\nmodule.exports = {\n  init (passport, conf) {\n    // Workaround for Chrome's SameSite cookies\n    // cookieSameSite needs useCookieInsteadOfSession to work correctly.\n    // cookieEncryptionKeys is extracted from conf.cookieEncryptionKeyString.\n    // It's a concatnation of 44-character length strings each of which represents a single pair of key/iv.\n    // Valid cookieEncryptionKeys enables both cookieSameSite and useCookieInsteadOfSession.\n    const keyArray = [];\n    if (conf.cookieEncryptionKeyString) {\n      let keyString = conf.cookieEncryptionKeyString;\n      while (keyString.length >= 44) {\n        keyArray.push({ key: keyString.substring(0, 32), iv: keyString.substring(32, 44) });\n        keyString = keyString.substring(44);\n      }\n    }\n    passport.use(conf.key,\n      new OIDCStrategy({\n        identityMetadata: conf.entryPoint,\n        clientID: conf.clientId,\n        redirectUrl: conf.callbackURL,\n        responseType: 'id_token',\n        responseMode: 'form_post',\n        scope: ['profile', 'email', 'openid'],\n        allowHttpForRedirectUrl: WIKI.IS_DEBUG,\n        passReqToCallback: true,\n        cookieSameSite: keyArray.length > 0,\n        useCookieInsteadOfSession: keyArray.length > 0,\n        cookieEncryptionKeys: keyArray\n      }, async (req, iss, sub, profile, cb) => {\n        const usrEmail = _.get(profile, '_json.email', null) || _.get(profile, '_json.preferred_username')\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              id: profile.oid,\n              displayName: profile.displayName,\n              email: usrEmail,\n              picture: ''\n            }\n          })\n          if (conf.mapGroups) {\n            const groups = _.get(profile, '_json.groups')\n            if (groups && _.isArray(groups)) {\n              const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)\n              const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)\n              for (const groupId of _.difference(expectedGroups, currentGroups)) {\n                await user.$relatedQuery('groups').relate(groupId)\n              }\n              for (const groupId of _.difference(currentGroups, expectedGroups)) {\n                await user.$relatedQuery('groups').unrelate().where('groupId', groupId)\n              }\n            }\n          }\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/azure/definition.yml",
    "content": "key: azure\ntitle: Azure Active Directory\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/azure.svg\ncolor: blue darken-3\nwebsite: https://azure.microsoft.com/services/active-directory/\nisAvailable: true\nuseForm: false\nscopes:\n  - profile\n  - email\n  - openid\nprops:\n  entryPoint:\n    type: String\n    title: Identity Metadata Endpoint\n    hint: The metadata endpoint provided by the Microsoft Identity Portal that provides the keys and other important information at runtime.\n    order: 1\n  clientId:\n    type: String\n    title: Client ID\n    hint: The client ID of your application in AAD (Azure Active Directory)\n    order: 2\n  cookieEncryptionKeyString:\n    type: String\n    title: Cookie Encryption Key String\n    hint: Random string with 44-character length.  Setting this enables workaround for Chrome's SameSite cookies.\n    order: 3\n  mapGroups:\n    type: Boolean\n    title: Map Groups\n    hint: Map groups matching names from the groups claim value\n    default: false\n    order: 4\n"
  },
  {
    "path": "server/modules/authentication/cas/authentication.js",
    "content": "const _ = require('lodash')\n/* global WIKI */\n\n// ------------------------------------\n// CAS Account\n// ------------------------------------\n\nconst CASStrategy = require('passport-cas').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new CASStrategy({\n        version: conf.casVersion,\n        ssoBaseURL: conf.casUrl,\n        serverBaseURL: conf.baseUrl,\n        serviceURL: conf.callbackURL,\n        passReqToCallback: true\n      }, async (req, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              id: _.get(profile.attributes, conf.uniqueIdAttribute, profile.user),\n              email: _.get(profile.attributes, conf.emailAttribute),\n              name: _.get(profile.attributes, conf.displayNameAttribute, profile.user),\n              picture: ''\n            }\n          })\n\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/cas/definition.yml",
    "content": "key: cas\ntitle: CAS\ndescription: The Central Authentication Service (CAS) is a single sign-on protocol for the web.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/cas.svg\ncolor: green darken-2\nwebsite: https://apereo.github.io/cas/\nuseForm: false\nisAvailable: true\nprops:\n  baseUrl:\n    type: String\n    title: Base URL\n    hint: 'Base-URL of your WikiJS (for example: https://wiki.example.com)'\n    order: 1\n  casUrl:\n    type: String\n    title: URL to the CAS Server\n    hint: 'Base-URL of the CAS server, including context path. (for example: https://login.company.com/cas)'\n    order: 2\n  casVersion:\n    type: String\n    title: CAS Version\n    hint: 'The version of CAS to use'\n    order: 3\n    enum:\n      - CAS3.0\n      - CAS1.0\n    default: 'CAS3.0'\n  emailAttribute:\n    type: String\n    title: Attribute key which contains the users email\n    default: email\n    order: 4\n  displayNameAttribute:\n    type: String\n    title: Attribute key which contains the users display name (leave empty if there is none)\n    order: 5\n  uniqueIdAttribute:\n    type: String\n    title: Attribute key which contains the unique identifier of a user. (if empty, username will be used)\n    order: 6\n"
  },
  {
    "path": "server/modules/authentication/discord/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Discord Account\n// ------------------------------------\n\nconst DiscordStrategy = require('passport-discord').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new DiscordStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none',\n        callbackURL: conf.callbackURL,\n        scope: 'identify email guilds',\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          if (conf.guildId && !_.some(profile.guilds, { id: conf.guildId })) {\n            throw new WIKI.Error.AuthLoginFailed()\n          }\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              displayName: profile.username,\n              picture: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/discord/definition.yml",
    "content": "key: discord\ntitle: Discord\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/discord.svg\ncolor: indigo lighten-2\nwebsite: https://discord.com/\nisAvailable: true\nuseForm: false\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  guildId:\n    type: String\n    title: Server ID\n    hint: Optional - Your unique server identifier, such that only members are authorized\n    order: 3\n"
  },
  {
    "path": "server/modules/authentication/dropbox/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Dropbox Account\n// ------------------------------------\n\nconst DropboxStrategy = require('passport-dropbox-oauth2').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new DropboxStrategy({\n        apiVersion: '2',\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, '_json.profile_photo_url', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/dropbox/definition.yml",
    "content": "key: dropbox\ntitle: Dropbox\ndescription: Dropbox is a file hosting service that offers cloud storage, file synchronization, personal cloud, and client software.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/dropbox.svg\ncolor: blue darken-2\nwebsite: https://dropbox.com\nisAvailable: true\nuseForm: false\nprops:\n  clientId:\n    type: String\n    title: App Key\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: App Secret\n    hint: Application Client Secret\n    order: 2\n"
  },
  {
    "path": "server/modules/authentication/facebook/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Facebook Account\n// ------------------------------------\n\nconst FacebookStrategy = require('passport-facebook').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new FacebookStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        profileFields: ['id', 'displayName', 'email', 'photos'],\n        authType: 'reauthenticate',\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, 'photos[0].value', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/facebook/definition.yml",
    "content": "key: facebook\ntitle: Facebook\ndescription: Facebook is an online social media and social networking service company.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/facebook.svg\ncolor: indigo\nwebsite: https://facebook.com/\nisAvailable: true\nuseForm: false\nscopes:\n  - email\nprops:\n  clientId:\n    type: String\n    title: App ID\n    hint: Application ID\n    order: 1\n  clientSecret:\n    type: String\n    title: App Secret\n    hint: Application Secret\n    order: 2\n"
  },
  {
    "path": "server/modules/authentication/firebase/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Firebase Account\n// ------------------------------------\n\n// INCOMPLETE / TODO\n\nconst FirebaseStrategy = require('passport-github2').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new FirebaseStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        scope: ['user:email']\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, 'photos[0].value', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/firebase/definition.yml",
    "content": "key: firebase\ntitle: Firebase\ndescription: Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/firebase.svg\ncolor: yellow darken-3\nwebsite: https://firebase.google.com/\nisAvailable: false\nuseForm: false\nprops: {}\n\n"
  },
  {
    "path": "server/modules/authentication/github/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// GitHub Account\n// ------------------------------------\n\nconst GitHubStrategy = require('passport-github2').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    let githubConfig = {\n      clientID: conf.clientId,\n      clientSecret: conf.clientSecret,\n      callbackURL: conf.callbackURL,\n      scope: ['user:email'],\n      passReqToCallback: true\n    }\n\n    if (conf.useEnterprise) {\n      githubConfig.authorizationURL = `https://${conf.enterpriseDomain}/login/oauth/authorize`\n      githubConfig.tokenURL = `https://${conf.enterpriseDomain}/login/oauth/access_token`\n      githubConfig.userProfileURL = conf.enterpriseUserEndpoint\n      githubConfig.userEmailURL = `${conf.enterpriseUserEndpoint}/emails`\n    }\n\n    passport.use(conf.key,\n      new GitHubStrategy(githubConfig, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          WIKI.logger.info(`GitHub OAuth: Processing profile for user ${profile.id || profile.username}`)\n          \n          // Ensure email is available - passport-github2 should fetch it automatically with user:email scope\n          // but we'll log a warning if it's missing\n          if (!profile.emails || (Array.isArray(profile.emails) && profile.emails.length === 0)) {\n            WIKI.logger.warn(`GitHub OAuth: No email found in profile for user ${profile.id || profile.username}. Make sure 'user:email' scope is granted.`)\n          }\n\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, 'photos[0].value', '')\n            }\n          })\n          \n          WIKI.logger.info(`GitHub OAuth: Successfully authenticated user ${user.email}`)\n          cb(null, user)\n        } catch (err) {\n          WIKI.logger.warn(`GitHub OAuth: Authentication failed for strategy ${req.params.strategy}:`, err)\n          // Provide more user-friendly error messages\n          if (err.message && err.message.includes('email')) {\n            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)\n          } else if (err instanceof WIKI.Error.AuthAccountBanned) {\n            cb(err, null)\n          } else {\n            cb(new Error(`GitHub authentication failed: ${err.message || 'Unknown error'}`), null)\n          }\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/github/definition.yml",
    "content": "key: github\ntitle: GitHub\ndescription: GitHub Inc. is a web-based hosting service for version control using Git.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/github.svg\ncolor: grey darken-3\nwebsite: https://github.com\nisAvailable: true\nuseForm: false\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  useEnterprise:\n    type: Boolean\n    title: Use GitHub Enterprise\n    hint: Enable if you're using the self-hosted GitHub Enterprise version\n    default: false\n    order: 3\n  enterpriseDomain:\n    type: String\n    title: GitHub Enterprise Domain\n    hint: GitHub Enterprise Only - Domain of your installation (e.g. github.company.com). Leave blank otherwise.\n    default: ''\n    order: 4\n  enterpriseUserEndpoint:\n    type: String\n    title: GitHub Enterprise User Endpoint\n    hint: GitHub Enterprise Only - Endpoint to fetch user details (e.g. https://api.github.com/user). Leave blank otherwise.\n    default: 'https://api.github.com/user'\n    order: 5\n\n"
  },
  {
    "path": "server/modules/authentication/gitlab/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// GitLab Account\n// ------------------------------------\n\nconst GitLabStrategy = require('passport-gitlab2').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new GitLabStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        baseURL: conf.baseUrl,\n        authorizationURL: conf.authorizationURL || (conf.baseUrl + '/oauth/authorize'),\n        tokenURL: conf.tokenURL || (conf.baseUrl + '/oauth/token'),\n        scope: ['read_user'],\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, 'avatarUrl', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/gitlab/definition.yml",
    "content": "key: gitlab\ntitle: GitLab\ndescription: GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager providing wiki, issue-tracking and CI/CD pipeline features.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/gitlab.svg\ncolor: deep-orange\nwebsite: https://gitlab.com\nisAvailable: true\nuseForm: false\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  baseUrl:\n    type: String\n    title: Base URL\n    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).\n    default: https://gitlab.com\n    order: 3\n  authorizationURL:\n    type: String\n    title: Authorization URL\n    hint: For self-managed GitLab instances, define an alternate authorization URL (e.g. http://example.com/oauth/authorize). Leave empty otherwise.\n    order: 4\n  tokenURL:\n    type: String\n    title: Token URL\n    hint: For self-managed GitLab instances, define an alternate token URL (e.g. http://example.com/oauth/token). Leave empty otherwise.\n    order: 5\n"
  },
  {
    "path": "server/modules/authentication/google/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Google ID Account\n// ------------------------------------\n\nconst GoogleStrategy = require('passport-google-oauth20').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    const strategy = new GoogleStrategy({\n      clientID: conf.clientId,\n      clientSecret: conf.clientSecret,\n      callbackURL: conf.callbackURL,\n      passReqToCallback: true\n    }, async (req, accessToken, refreshToken, profile, cb) => {\n      try {\n        WIKI.logger.info(`Google OAuth: Processing profile for user ${profile.id || profile.displayName}`)\n        \n        // Validate hosted domain if configured\n        if (conf.hostedDomain && profile._json.hd !== conf.hostedDomain) {\n          throw new Error(`Google authentication failed: User must be from domain ${conf.hostedDomain}, but got ${profile._json.hd || 'unknown'}`)\n        }\n\n        const user = await WIKI.models.users.processProfile({\n          providerKey: req.params.strategy,\n          profile: {\n            ...profile,\n            picture: _.get(profile, 'photos[0].value', '')\n          }\n        })\n        \n        WIKI.logger.info(`Google OAuth: Successfully authenticated user ${user.email}`)\n        cb(null, user)\n      } catch (err) {\n        WIKI.logger.warn(`Google OAuth: Authentication failed for strategy ${req.params.strategy}:`, err)\n        // Provide more user-friendly error messages\n        if (err.message && err.message.includes('domain')) {\n          cb(new Error(`Google authentication failed: ${err.message}`), null)\n        } else if (err.message && err.message.includes('email')) {\n          cb(new Error('Google authentication failed: Email address is required but not available. Please ensure your Google account has a verified email address.'), null)\n        } else if (err instanceof WIKI.Error.AuthAccountBanned) {\n          cb(err, null)\n        } else {\n          cb(new Error(`Google authentication failed: ${err.message || 'Unknown error'}`), null)\n        }\n      }\n    })\n\n    if (conf.hostedDomain) {\n      strategy.authorizationParams = function(options) {\n        return {\n          hd: conf.hostedDomain\n        }\n      }\n    }\n\n    passport.use(conf.key, strategy)\n  },\n  logout (conf) {\n    return '/'\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/google/definition.yml",
    "content": "key: google\ntitle: Google\ndescription: Google specializes in Internet-related services and products, which include online advertising technologies, search engine, cloud computing, software, and hardware.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/google.svg\ncolor: red darken-1\nwebsite: https://console.developers.google.com/\nisAvailable: true\nuseForm: false\nscopes:\n  - profile\n  - email\n  - openid\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  hostedDomain:\n    type: String\n    title: Hosted Domain\n    hint: (optional) Only for G Suite hosted domain. Leave empty otherwise.\n    order: 3\n"
  },
  {
    "path": "server/modules/authentication/keycloak/authentication.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n// ------------------------------------\n// Keycloak Account\n// ------------------------------------\n\nconst KeycloakStrategy = require('@exlinc/keycloak-passport')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new KeycloakStrategy({\n        authorizationURL: conf.authorizationURL,\n        userInfoURL: conf.userInfoURL,\n        tokenURL: conf.tokenURL,\n        host: conf.host,\n        realm: conf.realm,\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, results, profile, cb) => {\n        let displayName = profile.username\n        if (_.isString(profile.fullName) && profile.fullName.length > 0) {\n          displayName = profile.fullName\n        }\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              id: profile.keycloakId,\n              email: profile.email,\n              name: displayName,\n              picture: ''\n            }\n          })\n          req.session.keycloak_id_token = results.id_token\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  },\n  logout (conf, context) {\n    if (!conf.logoutUpstream) {\n      return '/'\n    } else if (conf.logoutURL && conf.logoutURL.length > 5) {\n      const idToken = context.req.session.keycloak_id_token\n      const redirURL = encodeURIComponent(WIKI.config.host)\n      if (conf.logoutUpstreamRedirectLegacy) {\n        // keycloak < 18\n        return `${conf.logoutURL}?redirect_uri=${redirURL}`\n      } else if (idToken) {\n        // keycloak 18+\n        return `${conf.logoutURL}?post_logout_redirect_uri=${redirURL}&id_token_hint=${idToken}`\n      } else {\n        // fall back to no redirect if keycloak_id_token isn't available\n        return conf.logoutURL\n      }\n    } else {\n      WIKI.logger.warn('Keycloak logout URL is not configured!')\n      return '/'\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/keycloak/definition.yml",
    "content": "key: keycloak\ntitle: Keycloak\ndescription: Keycloak is an open source software product to allow single sign-on with Identity Management and Access Management aimed at modern applications and services.\nauthor: D4uS1\nlogo: https://static.requarks.io/logo/keycloak.svg\ncolor: blue-grey darken-2\nwebsite: https://www.keycloak.org/\nuseForm: false\nisAvailable: true\nscopes:\n  - openid\n  - profile\n  - email\nprops:\n  host:\n    type: String\n    title: Host\n    hint: e.g. https://your.keycloak-host.com\n    order: 1\n  realm:\n    type: String\n    title: Realm\n    hint: The realm this application belongs to.\n    order: 2\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 3\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 4\n  authorizationURL:\n    type: String\n    title: Authorization Endpoint URL\n    hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/auth\n    order: 5\n  tokenURL:\n    type: String\n    title: Token Endpoint URL\n    hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/token\n    order: 6\n  userInfoURL:\n    type: String\n    title: User Info Endpoint URL\n    hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/userinfo\n    order: 7\n  logoutUpstream:\n    type: Boolean\n    title: Logout from Keycloak on Logout\n    hint: Should the user be redirected to Keycloak logout mechanism upon logout\n    order: 8\n  logoutURL:\n    type: String\n    title: Logout Endpoint URL\n    hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/logout\n    order: 9\n  logoutUpstreamRedirectLegacy:\n    type: Boolean\n    title: Legacy Logout Redirect\n    hint: Pass the legacy 'redirect_uri' parameter to the logout endpoint. Leave disabled for Keycloak 18 and above.\n    order: 10\n\n"
  },
  {
    "path": "server/modules/authentication/ldap/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// LDAP Account\n// ------------------------------------\n\nconst LdapStrategy = require('passport-ldapauth').Strategy\nconst fs = require('fs')\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new LdapStrategy({\n        server: {\n          url: conf.url,\n          bindDn: conf.bindDn,\n          bindCredentials: conf.bindCredentials,\n          searchBase: conf.searchBase,\n          searchFilter: conf.searchFilter,\n          tlsOptions: getTlsOptions(conf),\n          ...conf.mapGroups && {\n            groupSearchBase: conf.groupSearchBase,\n            groupSearchFilter: conf.groupSearchFilter,\n            groupSearchScope: conf.groupSearchScope,\n            groupDnProperty: conf.groupDnProperty,\n            groupSearchAttributes: [conf.groupNameField]\n          },\n          includeRaw: true\n        },\n        usernameField: 'email',\n        passwordField: 'password',\n        passReqToCallback: true\n      }, async (req, profile, cb) => {\n        try {\n          const userId = _.get(profile, conf.mappingUID, null)\n          if (!userId) {\n            throw new Error('Invalid Unique ID field mapping!')\n          }\n\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              id: userId,\n              email: String(_.get(profile, conf.mappingEmail, '')).split(',')[0],\n              displayName: _.get(profile, conf.mappingDisplayName, '???'),\n              picture: _.get(profile, `_raw.${conf.mappingPicture}`, '')\n            }\n          })\n          // map users LDAP groups to wiki groups with the same name, and remove any groups that don't match LDAP\n          if (conf.mapGroups) {\n            const ldapGroups = _.get(profile, '_groups')\n            if (ldapGroups && _.isArray(ldapGroups)) {\n              const groups = ldapGroups.map(g => g[conf.groupNameField])\n              const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)\n              const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)\n              for (const groupId of _.difference(expectedGroups, currentGroups)) {\n                await user.$relatedQuery('groups').relate(groupId)\n              }\n              for (const groupId of _.difference(currentGroups, expectedGroups)) {\n                await user.$relatedQuery('groups').unrelate().where('groupId', groupId)\n              }\n            }\n          }\n          cb(null, user)\n        } catch (err) {\n          if (WIKI.config.flags.ldapdebug) {\n            WIKI.logger.warn('LDAP LOGIN ERROR (c2): ', err)\n          }\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n\nfunction getTlsOptions(conf) {\n  if (!conf.tlsEnabled) {\n    return {}\n  }\n\n  if (!conf.tlsCertPath) {\n    return {\n      rejectUnauthorized: conf.verifyTLSCertificate\n    }\n  }\n\n  const caList = []\n  if (conf.verifyTLSCertificate) {\n    caList.push(fs.readFileSync(conf.tlsCertPath))\n  }\n\n  return {\n    rejectUnauthorized: conf.verifyTLSCertificate,\n    ca: caList\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/ldap/definition.yml",
    "content": "key: ldap\ntitle: LDAP / Active Directory\ndescription: Active Directory is a directory service that Microsoft developed for the Windows domain networks.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/active-directory.svg\ncolor: blue darken-3\nwebsite: https://www.microsoft.com/windowsserver\nisAvailable: true\nuseForm: true\nusernameType: username\nprops:\n  url:\n    title: LDAP URL\n    type: String\n    default: 'ldap://serverhost:389'\n    hint: (e.g. ldap://serverhost:389 or ldaps://serverhost:636)\n    order: 1\n  bindDn:\n    title: Admin Bind DN\n    type: String\n    default: cn='root'\n    hint: The distinguished name (dn) of the account used for binding.\n    maxWidth: 600\n    order: 2\n  bindCredentials:\n    title: Admin Bind Credentials\n    type: String\n    hint: The password of the account used above for binding.\n    maxWidth: 600\n    order: 3\n  searchBase:\n    title: Search Base\n    type: String\n    default: 'o=users,o=example.com'\n    hint: The base DN from which to search for users.\n    order: 4\n  searchFilter:\n    title: Search Filter\n    type: String\n    default: '(uid={{username}})'\n    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.\n    order: 5\n  tlsEnabled:\n    title: Use TLS\n    type: Boolean\n    default: false\n    order: 6\n  verifyTLSCertificate:\n    title: Verify TLS Certificate\n    type: Boolean\n    default: true\n    order: 7\n  tlsCertPath:\n    title: TLS Certificate Path\n    type: String\n    hint: Absolute path to the TLS certificate on the server.\n    order: 8\n  mappingUID:\n    title: Unique ID Field Mapping\n    type: String\n    default: 'uid'\n    hint: The field storing the user unique identifier. Usually \"uid\" or \"sAMAccountName\".\n    maxWidth: 500\n    order: 20\n  mappingEmail:\n    title: Email Field Mapping\n    type: String\n    default: 'mail'\n    hint: The field storing the user email. Usually \"mail\".\n    maxWidth: 500\n    order: 21\n  mappingDisplayName:\n    title: Display Name Field Mapping\n    type: String\n    default: 'displayName'\n    hint: The field storing the user display name. Usually \"displayName\" or \"cn\".\n    maxWidth: 500\n    order: 22\n  mappingPicture:\n    title: Avatar Picture Field Mapping\n    type: String\n    default: 'jpegPhoto'\n    hint: The field storing the user avatar picture. Usually \"jpegPhoto\" or \"thumbnailPhoto\".\n    maxWidth: 500\n    order: 23\n  mapGroups:\n    type: Boolean\n    title: Map Groups\n    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.\n    default: false\n    order: 24\n  groupSearchBase:\n    type: String\n    title: Group Search Base\n    hint: The base DN from which to search for groups.\n    default: OU=groups,dc=example,dc=com\n    order: 25\n  groupSearchFilter:\n    type: String\n    title: Group Search Filter\n    hint: LDAP search filter for groups. (member={{dn}}) will use the distinguished name of the user and will work in most cases.\n    default: (member={{dn}})\n    order: 26\n  groupSearchScope:\n    type: String\n    title: Group Search Scope\n    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.\n    default: sub\n    order: 27\n  groupDnProperty:\n    type: String\n    title: Group DN Property\n    hint: The property of user object to use in {{dn}} interpolation of Group Search Filter.\n    default: dn\n    order: 28\n  groupNameField:\n    type: String\n    title: Group Name Field\n    hint: The field that contains the name of the LDAP group to match on, usually \"name\" or \"cn\".\n    default: name\n    order: 29\n"
  },
  {
    "path": "server/modules/authentication/local/authentication.js",
    "content": "const bcrypt = require('bcryptjs-then')\n\n/* global WIKI */\n\n// ------------------------------------\n// Local Account\n// ------------------------------------\n\nconst LocalStrategy = require('passport-local').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use('local',\n      new LocalStrategy({\n        usernameField: 'email',\n        passwordField: 'password'\n      }, async (uEmail, uPassword, done) => {\n        try {\n          const user = await WIKI.models.users.query().findOne({\n            email: uEmail.toLowerCase(),\n            providerKey: 'local'\n          })\n          if (user) {\n            await user.verifyPassword(uPassword)\n            if (!user.isActive) {\n              done(new WIKI.Error.AuthAccountBanned(), null)\n            } else if (!user.isVerified) {\n              done(new WIKI.Error.AuthAccountNotVerified(), null)\n            } else {\n              done(null, user)\n            }\n          } else {\n            // Fake verify password to mask timing differences\n            await bcrypt.compare((Math.random() + 1).toString(36), '$2a$12$irXbAcQSY59pcQQfNQpY8uyhfSw48nzDikAmr60drI501nR.PuBx2')\n\n            done(new WIKI.Error.AuthLoginFailed(), null)\n          }\n        } catch (err) {\n          done(err, null)\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/local/definition.yml",
    "content": "key: local\ntitle: Local Database\ndescription: Built-in authentication for Wiki.js\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/wikijs.svg\ncolor: primary\nwebsite: https://wiki.js.org\nisAvailable: true\nuseForm: true\nusernameType: email\nprops: {}\n"
  },
  {
    "path": "server/modules/authentication/microsoft/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Microsoft Account\n// ------------------------------------\n\nconst WindowsLiveStrategy = require('passport-microsoft').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new WindowsLiveStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        scope: ['User.Read', 'email', 'openid', 'profile'],\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, 'photos[0].value', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/microsoft/definition.yml",
    "content": "key: microsoft\ntitle: Microsoft\ndescription: Microsoft is a software company, best known for it's Windows, Office, Azure, Xbox and Surface products.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/microsoft.svg\ncolor: blue\nwebsite: https://apps.dev.microsoft.com/\nisAvailable: false\nuseForm: false\nscopes:\n  - openid\n  - profile\n  - email\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n"
  },
  {
    "path": "server/modules/authentication/oauth2/authentication.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n// ------------------------------------\n// OAuth2 Account\n// ------------------------------------\n\nconst OAuth2Strategy = require('passport-oauth2').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    var client = new OAuth2Strategy({\n      authorizationURL: conf.authorizationURL,\n      tokenURL: conf.tokenURL,\n      clientID: conf.clientId,\n      clientSecret: conf.clientSecret,\n      userInfoURL: conf.userInfoURL,\n      callbackURL: conf.callbackURL,\n      passReqToCallback: true,\n      scope: conf.scope,\n      state: conf.enableCSRFProtection\n    }, async (req, accessToken, refreshToken, profile, cb) => {\n      try {\n        const picture = _.get(profile, conf.pictureClaim, '')\n        const user = await WIKI.models.users.processProfile({\n          providerKey: req.params.strategy,\n          profile: {\n            ...profile,\n            id: _.get(profile, conf.userIdClaim),\n            displayName: _.get(profile, conf.displayNameClaim, '???'),\n            email: _.get(profile, conf.emailClaim),\n            picture: picture\n          }\n        })\n        if (conf.mapGroups) {\n          const groups = _.get(profile, conf.groupsClaim)\n          if (groups && _.isArray(groups)) {\n            const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)\n            const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)\n            for (const groupId of _.difference(expectedGroups, currentGroups)) {\n              await user.$relatedQuery('groups').relate(groupId)\n            }\n            for (const groupId of _.difference(currentGroups, expectedGroups)) {\n              await user.$relatedQuery('groups').unrelate().where('groupId', groupId)\n            }\n          }\n        }\n        cb(null, user)\n      } catch (err) {\n        cb(err, null)\n      }\n    })\n\n    client.userProfile = function (accesstoken, done) {\n      this._oauth2._useAuthorizationHeaderForGET = !conf.useQueryStringForAccessToken\n      this._oauth2.get(conf.userInfoURL, accesstoken, (err, data) => {\n        if (err) {\n          return done(err)\n        }\n        try {\n          data = JSON.parse(data)\n        } catch (e) {\n          return done(e)\n        }\n        done(null, data)\n      })\n    }\n    passport.use(conf.key, client)\n  },\n  logout (conf) {\n    if (!conf.logoutURL) {\n      return '/'\n    } else {\n      return conf.logoutURL\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/oauth2/definition.yml",
    "content": "key: oauth2\ntitle: Generic OAuth2\ndescription: OAuth 2.0 is the industry-standard protocol for authorization.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/oauth2.svg\ncolor: blue-grey darken-2\nwebsite: https://oauth.net/2/\nisAvailable: true\nuseForm: false\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  authorizationURL:\n    type: String\n    title: Authorization Endpoint URL\n    hint: Application Authorization Endpoint URL\n    order: 3\n  tokenURL:\n    type: String\n    title: Token Endpoint URL\n    hint: Application Token Endpoint URL\n    order: 4\n  userInfoURL:\n    type: String\n    title: User Info Endpoint URL\n    hint: User Info Endpoint URL\n    order: 5\n  userIdClaim:\n    type: String\n    title: ID Claim\n    hint: Field containing the user ID\n    default: id\n    maxWidth: 500\n    order: 6\n  displayNameClaim:\n    type: String\n    title: Display Name Claim\n    hint: Field containing user display name\n    default: displayName\n    maxWidth: 500\n    order: 7\n  emailClaim:\n    type: String\n    title: Email Claim\n    hint: Field containing the user email address\n    default: email\n    maxWidth: 500\n    order: 8\n  pictureClaim:\n    type: String\n    title: Picture Claim\n    hint: Field containing the user avatar URL\n    default: picture\n    maxWidth: 500\n    order: 9\n  mapGroups:\n    type: Boolean\n    title: Map Groups\n    hint: Map groups matching names from the groups claim value\n    default: false\n    order: 10\n  groupsClaim:\n    type: String\n    title: Groups Claim\n    hint: Field containing the group names\n    default: groups\n    maxWidth: 500\n    order: 11\n  logoutURL:\n    type: String\n    title: Logout URL\n    hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.\n    order: 12\n  scope:\n    type: String\n    title: Scope\n    hint: (optional) Application Client permission scopes.\n    order: 13\n  useQueryStringForAccessToken:\n    type: Boolean\n    default: false\n    title: Pass access token via GET query string to User Info Endpoint\n    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.\n    order: 14\n  enableCSRFProtection:\n    type: Boolean\n    default: true\n    title: Enable CSRF protection\n    hint: Pass a nonce state parameter during authentication to protect against CSRF attacks.\n    order: 15\n"
  },
  {
    "path": "server/modules/authentication/oidc/authentication.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n// ------------------------------------\n// OpenID Connect Account\n// ------------------------------------\n\nconst OpenIDConnectStrategy = require('passport-openidconnect').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new OpenIDConnectStrategy({\n        authorizationURL: conf.authorizationURL,\n        tokenURL: conf.tokenURL,\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        issuer: conf.issuer,\n        userInfoURL: conf.userInfoURL,\n        callbackURL: conf.callbackURL,\n        passReqToCallback: true,\n        skipUserProfile: conf.skipUserProfile,\n        acrValues: conf.acrValues\n      }, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => {\n        const profile = Object.assign({}, idProfile, uiProfile)\n        const picture = _.get(profile, '_json.' + conf.pictureClaim, '')\n\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              email: _.get(profile, '_json.' + conf.emailClaim),\n              displayName: _.get(profile, '_json.' + conf.displayNameClaim, ''),\n              picture: picture\n            }\n          })\n          if (conf.mapGroups) {\n            const groups = _.get(profile, '_json.' + conf.groupsClaim)\n            if (groups && _.isArray(groups)) {\n              const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)\n              const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)\n              for (const groupId of _.difference(expectedGroups, currentGroups)) {\n                await user.$relatedQuery('groups').relate(groupId)\n              }\n              for (const groupId of _.difference(currentGroups, expectedGroups)) {\n                await user.$relatedQuery('groups').unrelate().where('groupId', groupId)\n              }\n            }\n          }\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  },\n  logout (conf) {\n    if (!conf.logoutURL) {\n      return '/'\n    } else {\n      return conf.logoutURL\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/oidc/definition.yml",
    "content": "key: oidc\ntitle: Generic OpenID Connect / OAuth2\ndescription: OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/oidc.svg\ncolor: blue-grey darken-2\nwebsite: http://openid.net/connect/\nisAvailable: true\nuseForm: false\nscopes:\n  - openid\n  - profile\n  - email\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  authorizationURL:\n    type: String\n    title: Authorization Endpoint URL\n    hint: Application Authorization Endpoint URL\n    order: 3\n  tokenURL:\n    type: String\n    title: Token Endpoint URL\n    hint: Application Token Endpoint URL\n    order: 4\n  userInfoURL:\n    type: String\n    title: User Info Endpoint URL\n    hint: User Info Endpoint URL\n    order: 5\n  skipUserProfile:\n    type: Boolean\n    default: false\n    title: Skip User Profile\n    hint: Skips call to the OIDC UserInfo endpoint\n    order: 6\n  issuer:\n    type: String\n    title: Issuer\n    hint: Issuer URL\n    order: 7\n  emailClaim:\n    type: String\n    title: Email Claim\n    hint: Field containing the email address\n    default: email\n    maxWidth: 500\n    order: 8\n  displayNameClaim:\n    type: String\n    title: Display Name Claim\n    hint: Field containing the user display name\n    default: displayName\n    maxWidth: 500\n    order: 9\n  pictureClaim:\n    type: String\n    title: Picture Claim\n    hint: Field containing the user avatar URL\n    default: picture\n    maxWidth: 500\n    order: 10\n  mapGroups:\n    type: Boolean\n    title: Map Groups\n    hint: Map groups matching names from the groups claim value\n    default: false\n    order: 11\n  groupsClaim:\n    type: String\n    title: Groups Claim\n    hint: Field containing the group names\n    default: groups\n    maxWidth: 500\n    order: 12\n  logoutURL:\n    type: String\n    title: Logout URL\n    hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.\n    order: 13\n  acrValues:\n    type: String\n    title: ACR Values\n    hint: (optional) Authentication Context Class Reference\n    order: 14\n"
  },
  {
    "path": "server/modules/authentication/okta/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Okta Account\n// ------------------------------------\n\nconst OktaStrategy = require('passport-okta-oauth').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new OktaStrategy({\n        audience: conf.audience,\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        idp: conf.idp,\n        callbackURL: conf.callbackURL,\n        response_type: 'code',\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, '_json.profile', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/okta/definition.yml",
    "content": "key: okta\ntitle: Okta\ndescription: Okta provide secure identity management and single sign-on to any application.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/okta.svg\ncolor: blue darken-1\nwebsite: https://www.okta.com/\nisAvailable: true\nuseForm: false\nscopes:\n  - profile\n  - email\n  - openid\nprops:\n  audience:\n    title: Org URL\n    type: String\n    hint: Okta organization URL (e.g. https://example.okta.com, https://example.oktapreview.com), found on the Developer Dashboard, in the upper right.\n    order: 1\n  clientId:\n    title: Client ID\n    type: String\n    hint: 20 chars alphanumeric string\n    maxWidth: 400\n    order: 2\n  clientSecret:\n    title: Client Secret\n    type: String\n    hint: 40 chars alphanumeric string with a hyphen(s)\n    maxWidth: 600\n    order: 3\n  idp:\n    title: Identity Provider ID (idp)\n    type: String\n    hint: (Optional) - 20 chars alphanumeric string\n    maxWidth: 400\n    order: 4\n"
  },
  {
    "path": "server/modules/authentication/rocketchat/authentication.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n// ------------------------------------\n// Rocket.chat Account\n// ------------------------------------\n\nconst OAuth2Strategy = require('passport-oauth2').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    const siteURL = conf.siteURL.slice(-1) === '/' ? conf.siteURL.slice(0, -1) : conf.siteURL\n\n    const strategyInstance = new OAuth2Strategy({\n      authorizationURL: `${siteURL}/oauth/authorize`,\n      tokenURL: `${siteURL}/oauth/token`,\n      clientID: conf.clientId,\n      clientSecret: conf.clientSecret,\n      callbackURL: conf.callbackURL,\n      passReqToCallback: true\n    }, async (req, accessToken, refreshToken, profile, cb) => {\n      try {\n        const user = await WIKI.models.users.processProfile({\n          providerKey: req.params.strategy,\n          profile\n        })\n        cb(null, user)\n      } catch (err) {\n        cb(err, null)\n      }\n    })\n\n    strategyInstance.userProfile = function (accessToken, cb) {\n      this._oauth2.get(`${siteURL}/api/v1/me`, accessToken, (err, body, res) => {\n        if (err) {\n          WIKI.logger.warn('Rocket.chat - Failed to fetch user profile.')\n          return cb(err)\n        }\n        try {\n          const usr = JSON.parse(body)\n          cb(null, {\n            id: usr._id,\n            displayName: _.isEmpty(usr.name) ? usr.username : usr.name,\n            email: usr.emails[0].address,\n            picture: usr.avatarUrl\n          })\n        } catch (err) {\n          WIKI.logger.warn('Rocket.chat - Failed to parse user profile.')\n          cb(err)\n        }\n      })\n    }\n\n    passport.use(conf.key, strategyInstance)\n  },\n  logout (conf) {\n    if (!conf.logoutURL) {\n      return '/'\n    } else {\n      return conf.logoutURL\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/rocketchat/definition.yml",
    "content": "key: rocketchat\ntitle: Rocket.chat\ndescription: Communicate and collaborate with your team, share files, chat in real-time, or switch to video/audio conferencing.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/rocketchat.svg\ncolor: red accent-3\nwebsite: https://rocket.chat/\nisAvailable: true\nuseForm: false\nscopes:\n  - openid\n  - profile\n  - email\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  siteURL:\n    type: String\n    title: Rocket.chat Site URL\n    hint: The base URL of your Rocket.chat site (e.g. https://example.rocket.chat)\n    order: 3\n"
  },
  {
    "path": "server/modules/authentication/saml/authentication.js",
    "content": "const _ = require('lodash')\n\n/* global WIKI */\n\n// ------------------------------------\n// SAML Account\n// ------------------------------------\n\nconst SAMLStrategy = require('passport-saml').Strategy\n\nmodule.exports = {\n  init (passport, conf) {\n    const samlConfig = {\n      callbackUrl: conf.callbackURL,\n      entryPoint: conf.entryPoint,\n      issuer: conf.issuer,\n      cert: (conf.cert || '').split('|'),\n      signatureAlgorithm: conf.signatureAlgorithm,\n      digestAlgorithm: conf.digestAlgorithm,\n      identifierFormat: conf.identifierFormat,\n      wantAssertionsSigned: conf.wantAssertionsSigned,\n      acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs),\n      disableRequestedAuthnContext: conf.disableRequestedAuthnContext,\n      authnContext: (conf.authnContext || '').split('|'),\n      racComparison: conf.racComparison,\n      forceAuthn: conf.forceAuthn,\n      passive: conf.passive,\n      providerName: conf.providerName,\n      skipRequestCompression: conf.skipRequestCompression,\n      authnRequestBinding: conf.authnRequestBinding,\n      passReqToCallback: true\n    }\n    if (!_.isEmpty(conf.audience)) {\n      samlConfig.audience = conf.audience\n    }\n    if (!_.isEmpty(conf.privateKey)) {\n      samlConfig.privateKey = conf.privateKey\n    }\n    if (!_.isEmpty(conf.decryptionPvk)) {\n      samlConfig.decryptionPvk = conf.decryptionPvk\n    }\n    passport.use(conf.key,\n      new SAMLStrategy(samlConfig, async (req, profile, cb) => {\n        try {\n          const userId = _.get(profile, [conf.mappingUID], null) || _.get(profile, 'nameID', null)\n          if (!userId) {\n            throw new Error('Invalid or Missing Unique ID field!')\n          }\n\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              id: userId,\n              email: _.get(profile, conf.mappingEmail, ''),\n              displayName: _.get(profile, conf.mappingDisplayName, '???'),\n              picture: _.get(profile, conf.mappingPicture, '')\n            }\n          })\n\n          // map users provider groups to wiki groups with the same name, and remove any groups that don't match\n          // Code copied from the LDAP implementation with a slight variation on the field we extract the value from\n          // In SAML v2 groups come in profile.attributes and can be 1 string or an array of strings\n          if (conf.mapGroups) {\n            const maybeArrayOfGroups = _.get(profile.attributes, conf.mappingGroups)\n            const groups = (maybeArrayOfGroups && !_.isArray(maybeArrayOfGroups)) ? [maybeArrayOfGroups] : maybeArrayOfGroups\n\n            if (groups && _.isArray(groups)) {\n              const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)\n              const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)\n              for (const groupId of _.difference(expectedGroups, currentGroups)) {\n                await user.$relatedQuery('groups').relate(groupId)\n              }\n              for (const groupId of _.difference(currentGroups, expectedGroups)) {\n                await user.$relatedQuery('groups').unrelate().where('groupId', groupId)\n              }\n            }\n          }\n\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/saml/definition.yml",
    "content": "key: saml\ntitle: SAML 2.0\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/saml.svg\ncolor: red darken-3\nwebsite: https://wiki.oasis-open.org/security/FrontPage\nisAvailable: true\nuseForm: false\nprops:\n  entryPoint:\n    type: String\n    title: Entry Point\n    hint: Identity provider entrypoint (URL)\n    order: 1\n  issuer:\n    type: String\n    title: Issuer\n    hint: Issuer string to supply to Identity Provider\n    order: 2\n  audience:\n    type: String\n    title: Audience\n    hint: Expected SAML response Audience (if not provided, audience won't be verified)\n    order: 3\n  cert:\n    type: String\n    title: Certificate\n    hint: Public PEM-encoded X.509 signing certificate. If the provider has multiple certificates that are valid, join them together using the | pipe symbol.\n    multiline: true\n    order: 4\n  privateKey:\n    type: String\n    title: Private Key\n    hint: PEM formatted key used to sign the certificate.\n    multiline: true\n    order: 5\n  decryptionPvk:\n    type: String\n    title: Decryption Private Key\n    hint: (Optional) - Private key that will be used to attempt to decrypt any encrypted assertions that are received.\n    multiline: true\n    order: 6\n  signatureAlgorithm:\n    type: String\n    title: Signature Algorithm\n    hint: Signature algorithm used for signing requests\n    maxWidth: 400\n    order: 7\n    default: sha1\n    enum:\n      - sha1\n      - sha256\n      - sha512\n  digestAlgorithm:\n    type: String\n    title: Digest Algorithm\n    hint: Digest algorithm used to provide a digest for the signed data object\n    maxWidth: 400\n    order: 8\n    default: sha1\n    enum:\n      - sha1\n      - sha256\n      - sha512\n  identifierFormat:\n    type: String\n    title: Name Identifier format\n    default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'\n    order: 20\n  wantAssertionsSigned:\n    type: Boolean\n    title: Always sign assertions\n    hint: If enabled, add WantAssertionsSigned=\"true\" to the metadata, to specify that the IdP should always sign the assertions.\n    default: false\n    order: 21\n  acceptedClockSkewMs:\n    type: Number\n    title: Accepted Clock Skew Milleseconds\n    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.\n    default: 0\n    order: 22\n  disableRequestedAuthnContext:\n    type: Boolean\n    title: Disable Requested Auth Context\n    hint: If enabled, do not request a specific authentication context. This is known to help when authenticating against Active Directory (AD FS) servers.\n    default: false\n    order: 23\n  authnContext:\n    type: String\n    title: Auth Context\n    hint: Name identifier format to request auth context. For multiple values, join them together using the | pipe symbol.\n    default: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n    order: 24\n  racComparison:\n    type: String\n    title: RAC Comparison Type\n    hint: Requested Authentication Context comparison type.\n    maxWidth: 400\n    order: 25\n    default: exact\n    enum:\n      - exact\n      - minimum\n      - maximum\n      - better\n  forceAuthn:\n    type: Boolean\n    title: Force Initial Re-authentication\n    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.\n    default: false\n    order: 26\n  passive:\n    type: Boolean\n    title: Passive\n    hint: If enabled, the initial SAML request from the service provider specifies that the IdP should prevent visible user interaction.\n    default: false\n    order: 27\n  providerName:\n    type: String\n    title: Provider Name\n    hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider.\n    default: wiki.js\n    order: 28\n  skipRequestCompression:\n    type: Boolean\n    title: Skip Request Compression\n    hint: If enabled, the SAML request from the service provider won't be compressed.\n    default: false\n    order: 29\n  authnRequestBinding:\n    type: String\n    title: Request Binding\n    hint: Binding used for request authentication from IDP.\n    maxWidth: 400\n    order: 30\n    default: 'HTTP-POST'\n    enum:\n      - HTTP-Redirect\n      - HTTP-POST\n  mappingUID:\n    title: Unique ID Field Mapping\n    type: String\n    default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'\n    hint: The field storing the user unique identifier. Can be a variable name or a URI-formatted string.\n    order: 40\n  mappingEmail:\n    title: Email Field Mapping\n    type: String\n    default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'\n    hint: The field storing the user email. Can be a variable name or a URI-formatted string.\n    order: 41\n  mappingDisplayName:\n    title: Display Name Field Mapping\n    type: String\n    default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'\n    hint: The field storing the user display name. Can be a variable name or a URI-formatted string.\n    order: 42\n  mappingPicture:\n    title: Avatar Picture Field Mapping\n    type: String\n    default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/picture'\n    hint: The field storing the user avatar picture. Can be a variable name or a URI-formatted string.\n    order: 43\n  mapGroups:\n    type: Boolean\n    title: Map Groups\n    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.\n    default: false\n    order: 44\n  mappingGroups:\n    title: User Groups Field Mapping\n    type: String\n    default: 'memberOf'\n    hint: The field storing the user groups attribute (when Map Groups is enabled). Can be a variable name or a URI-formatted string.\n    order: 45\n"
  },
  {
    "path": "server/modules/authentication/slack/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Slack Account\n// ------------------------------------\n\nconst SlackStrategy = require('passport-slack-oauth2').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new SlackStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        team: conf.team,\n        scope: ['identity.basic', 'identity.email', 'identity.avatar'],\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, { user: userProfile }, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...userProfile,\n              picture: _.get(userProfile, 'image_48', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/slack/definition.yml",
    "content": "key: slack\ntitle: Slack\ndescription: Slack is a cloud-based set of proprietary team collaboration tools and services.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/slack.svg\ncolor: green\nwebsite: https://api.slack.com/docs/oauth\nisAvailable: true\nuseForm: false\nscopes:\n  - identity.basic\n  - identity.email\n  - identity.avatar\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n  team:\n    type: String\n    title: Team / Workspace ID\n    hint: Optional - Your unique team (workspace) identifier\n    order: 3\n"
  },
  {
    "path": "server/modules/authentication/twitch/authentication.js",
    "content": "/* global WIKI */\n\n// ------------------------------------\n// Twitch Account\n// ------------------------------------\n\nconst TwitchStrategy = require('passport-twitch-strategy').Strategy\nconst _ = require('lodash')\n\nmodule.exports = {\n  init (passport, conf) {\n    passport.use(conf.key,\n      new TwitchStrategy({\n        clientID: conf.clientId,\n        clientSecret: conf.clientSecret,\n        callbackURL: conf.callbackURL,\n        passReqToCallback: true\n      }, async (req, accessToken, refreshToken, profile, cb) => {\n        try {\n          const user = await WIKI.models.users.processProfile({\n            providerKey: req.params.strategy,\n            profile: {\n              ...profile,\n              picture: _.get(profile, 'profile_image_url', '')\n            }\n          })\n          cb(null, user)\n        } catch (err) {\n          cb(err, null)\n        }\n      }\n      ))\n  }\n}\n"
  },
  {
    "path": "server/modules/authentication/twitch/definition.yml",
    "content": "key: twitch\ntitle: Twitch\ndescription: Twitch is a live streaming video platform.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/twitch.svg\ncolor: indigo darken-2\nwebsite: https://dev.twitch.tv/docs/authentication/\nisAvailable: true\nuseForm: false\nscopes:\n  - 'user:read:email'\nprops:\n  clientId:\n    type: String\n    title: Client ID\n    hint: Application Client ID\n    order: 1\n  clientSecret:\n    type: String\n    title: Client Secret\n    hint: Application Client Secret\n    order: 2\n"
  },
  {
    "path": "server/modules/comments/artalk/code.yml",
    "content": "main: |\n  <div id=\"artalk-container\"></div>\nhead: |\n  <link href=\"{{server}}/dist/Artalk.css\" rel=\"stylesheet\">\n  <script src=\"{{server}}/dist/Artalk.js\"></script>\nbody: |\n  <script>\n    window.onload = function() {\n      Artalk.init({\n        el:        '#artalk-container',\n        pageKey:   '{{pageId}}',\n        pageTitle: '',\n        server:    '{{server}}',\n        site:      '{{siteName}}',\n      });\n    };\n  </script>\n"
  },
  {
    "path": "server/modules/comments/artalk/definition.yml",
    "content": "key: artalk\ntitle: Artalk\ndescription: A light-weight self-hosted comment system.\nauthor: CDN18\nlogo: https://static.requarks.io/logo/artalk.png\nwebsite: https://artalk.js.org\ncodeTemplate: true\nisAvailable: true\nprops:\n  server:\n    type: String\n    title: Artalk Backend URL\n    default: ''\n    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)'\n    maxWidth: 650\n    order: 1\n  siteName:\n    type: String\n    title: Site Name\n    default: ''\n    hint: 'The name of this site configured in the artalk backend. Leave empty to use default site.'\n    maxWidth: 450\n    order: 2\n"
  },
  {
    "path": "server/modules/comments/commento/code.yml",
    "content": "main: |\n  <div id=\"commento\"></div>\nbody: |\n  <script>\n    window.onload = function() {\n      var d = document, s = d.createElement('script');\n      s.src = '{{instanceUrl}}/js/commento.js';\n      s.defer = true\n      s.setAttribute('data-auto-init', true);\n      (d.head || d.body).appendChild(s);\n    };\n  </script>\n"
  },
  {
    "path": "server/modules/comments/commento/definition.yml",
    "content": "key: commento\ntitle: Commento\ndescription: A fast, privacy-focused commenting platform.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/commento.svg\nwebsite: https://commento.io/\ncodeTemplate: true\nisAvailable: true\nprops:\n  instanceUrl:\n    type: String\n    title: Instance URL\n    default: 'https://cdn.commento.io'\n    hint: The URL (without a trailing slash) to the Commento instance. Leave the default https://cdn.commento.io if using the cloud-hosted version.\n    order: 1\n\n"
  },
  {
    "path": "server/modules/comments/default/comment.js",
    "content": "const md = require('markdown-it')\nconst { full: mdEmoji } = require('markdown-it-emoji')\nconst { JSDOM } = require('jsdom')\nconst createDOMPurify = require('dompurify')\nconst _ = require('lodash')\nconst { AkismetClient } = require('akismet-api')\nconst moment = require('moment')\n\n/* global WIKI */\n\nconst window = new JSDOM('').window\nconst DOMPurify = createDOMPurify(window)\n\nlet akismetClient = null\n\nconst mkdown = md({\n  html: false,\n  breaks: true,\n  linkify: true,\n  highlight(str, lang) {\n    return `<pre><code class=\"language-${lang}\">${_.escape(str)}</code></pre>`\n  }\n})\n\nmkdown.use(mdEmoji)\n\n// ------------------------------------\n// Default Comment Provider\n// ------------------------------------\n\nmodule.exports = {\n  /**\n   * Init\n   */\n  async init (config) {\n    WIKI.logger.info('(COMMENTS/DEFAULT) Initializing...')\n    if (WIKI.data.commentProvider.config.akismet && WIKI.data.commentProvider.config.akismet.length > 2) {\n      akismetClient = new AkismetClient({\n        key: WIKI.data.commentProvider.config.akismet,\n        blog: WIKI.config.host,\n        lang: WIKI.config.lang.namespacing ? WIKI.config.lang.namespaces.join(', ') : WIKI.config.lang.code,\n        charset: 'UTF-8'\n      })\n      try {\n        const isValid = await akismetClient.verifyKey()\n        if (!isValid) {\n          akismetClient = null\n          WIKI.logger.warn('(COMMENTS/DEFAULT) Akismet Key is invalid! [ DISABLED ]')\n        } else {\n          WIKI.logger.info('(COMMENTS/DEFAULT) Akismet key is valid. [ OK ]')\n        }\n      } catch (err) {\n        akismetClient = null\n        WIKI.logger.warn('(COMMENTS/DEFAULT) Unable to verify Akismet Key: ' + err.message)\n      }\n    } else {\n      akismetClient = null\n    }\n    WIKI.logger.info('(COMMENTS/DEFAULT) Initialization completed.')\n  },\n  /**\n   * Create New Comment\n   */\n  async create ({ page, replyTo, content, user }) {\n    // -> Build New Comment\n    const newComment = {\n      content,\n      render: DOMPurify.sanitize(mkdown.render(content)),\n      replyTo,\n      pageId: page.id,\n      authorId: user.id,\n      name: user.name,\n      email: user.email,\n      ip: user.ip\n    }\n\n    // -> Check for Spam with Akismet\n    if (akismetClient) {\n      let userRole = 'user'\n      if (user.groups.indexOf(1) >= 0) {\n        userRole = 'administrator'\n      } else if (user.groups.indexOf(2) >= 0) {\n        userRole = 'guest'\n      }\n\n      let isSpam = false\n      try {\n        isSpam = await akismetClient.checkSpam({\n          ip: user.ip,\n          useragent: user.agentagent,\n          content,\n          name: user.name,\n          email: user.email,\n          permalink: `${WIKI.config.host}/${page.localeCode}/${page.path}`,\n          permalinkDate: page.updatedAt,\n          type: (replyTo > 0) ? 'reply' : 'comment',\n          role: userRole\n        })\n      } catch (err) {\n        WIKI.logger.warn('Akismet Comment Validation: [ FAILED ]')\n        WIKI.logger.warn(err)\n      }\n\n      if (isSpam) {\n        throw new Error('Comment was rejected because it is marked as spam.')\n      }\n    }\n\n    // -> Check for minimum delay between posts\n    if (WIKI.data.commentProvider.config.minDelay > 0) {\n      const lastComment = await WIKI.models.comments.query().select('updatedAt').findOne('authorId', user.id).orderBy('updatedAt', 'desc')\n      if (lastComment && moment().subtract(WIKI.data.commentProvider.config.minDelay, 'seconds').isBefore(lastComment.updatedAt)) {\n        throw new Error('Your administrator has set a time limit before you can post another comment. Try again later.')\n      }\n    }\n\n    // -> Save Comment to DB\n    const cm = await WIKI.models.comments.query().insert(newComment)\n\n    // -> Return Comment ID\n    return cm.id\n  },\n  /**\n   * Update an existing comment\n   */\n  async update ({ id, content, user }) {\n    const renderedContent = DOMPurify.sanitize(mkdown.render(content))\n    await WIKI.models.comments.query().findById(id).patch({\n      content,\n      render: renderedContent\n    })\n    return renderedContent\n  },\n  /**\n   * Delete an existing comment by ID\n   */\n  async remove ({ id, user }) {\n    return WIKI.models.comments.query().findById(id).delete()\n  },\n  /**\n   * Get the page ID from a comment ID\n   */\n  async getPageIdFromCommentId (id) {\n    const result = await WIKI.models.comments.query().select('pageId').findById(id)\n    return (result) ? result.pageId : false\n  },\n  /**\n   * Get a comment by ID\n   */\n  async getCommentById (id) {\n    return WIKI.models.comments.query().findById(id)\n  },\n  /**\n   * Get the total comments count for a page ID\n   */\n  async count (pageId) {\n    const result = await WIKI.models.comments.query().count('* as total').where('pageId', pageId).first()\n    return _.toSafeInteger(result.total)\n  }\n}\n"
  },
  {
    "path": "server/modules/comments/default/definition.yml",
    "content": "key: default\ntitle: Default\ndescription: Built-in advanced comments tool.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/wikijs-butterfly.svg\nwebsite: https://wiki.js.org\ncodeTemplate: false\nisAvailable: true\nprops:\n  akismet:\n    type: String\n    title: Akismet API Key\n    default: ''\n    hint: 'Prevent spam by using the Akismet service. Enter your API key here to enable. Leave empty to disable.'\n    maxWidth: 650\n    order: 1\n  minDelay:\n    type: Number\n    title: Post delay\n    default: 30\n    hint: 'Minimum delay (in seconds) between comments per account. Note that all guests are considered as a single account.'\n    maxWidth: 400\n    order: 2\n"
  },
  {
    "path": "server/modules/comments/disqus/code.yml",
    "content": "main: |\n  <div id=\"disqus_thread\"></div>\nbody: |\n  <script>\n    var disqus_config = function () {\n      this.page.url = '{{pageUrl}}';\n      this.page.identifier = '{{pageId}}';\n    };\n    (function() {\n      var d = document, s = d.createElement('script');\n      s.src = 'https://{{accountName}}.disqus.com/embed.js';\n      s.setAttribute('data-timestamp', +new Date());\n      (d.head || d.body).appendChild(s);\n    })();\n  </script>\n"
  },
  {
    "path": "server/modules/comments/disqus/definition.yml",
    "content": "key: disqus\ntitle: Disqus\ndescription: Disqus help publishers power online discussions with comments.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/disqus.svg\nwebsite: https://disqus.com/\ncodeTemplate: true\nisAvailable: true\nprops:\n  accountName:\n    type: String\n    title: Shortname\n    default: ''\n    hint: Unique identifier from Disqus to identify your website\n    order: 1\n"
  },
  {
    "path": "server/modules/editor/api/definition.yml",
    "content": "key: api\ntitle: API Docs\ndescription: REST / GraphQL Editor\ncontentType: yml\nauthor: requarks.io\nprops: {}\n"
  },
  {
    "path": "server/modules/editor/asciidoc/definition.yml",
    "content": "key: asciidoc\ntitle: Asciidoc\ndescription: Basic Asciidoc editor\ncontentType: asciidoc\nauthor: dzruyk\nprops: {}\n"
  },
  {
    "path": "server/modules/editor/ckeditor/definition.yml",
    "content": "key: ckeditor\ntitle: Visual Editor\ndescription: Rich-text WYSIWYG Editor\ncontentType: html\nauthor: requarks.io\nprops: {}\n"
  },
  {
    "path": "server/modules/editor/code/definition.yml",
    "content": "key: code\ntitle: Code\ndescription: Raw HTML editor\ncontentType: html\nauthor: requarks.io\nprops: {}\n"
  },
  {
    "path": "server/modules/editor/markdown/definition.yml",
    "content": "key: markdown\ntitle: Markdown\ndescription: Basic Markdown editor\ncontentType: markdown\nauthor: requarks.io\nprops: {}\n"
  },
  {
    "path": "server/modules/editor/redirect/definition.yml",
    "content": "key: redirect\ntitle: Redirection\ndescription: Redirect the user\ncontentType: redirect\nauthor: requarks.io\nprops: {}\n"
  },
  {
    "path": "server/modules/editor/wysiwyg/definition.yml",
    "content": "key: wysiwyg\ntitle: WYSIWYG\ndescription: Advanced Visual HTML Builder\ncontentType: html\nauthor: requarks.io\nprops: {}\n"
  },
  {
    "path": "server/modules/extensions/git/ext.js",
    "content": "const cmdExists = require('command-exists')\n\nmodule.exports = {\n  key: 'git',\n  title: 'Git',\n  description: 'Distributed version control system. Required for the Git storage module.',\n  isInstalled: false,\n  async isCompatible () {\n    return true\n  },\n  async check () {\n    try {\n      await cmdExists('git')\n      this.isInstalled = true\n    } catch (err) {\n      this.isInstalled = false\n    }\n    return this.isInstalled\n  }\n}\n"
  },
  {
    "path": "server/modules/extensions/pandoc/ext.js",
    "content": "const cmdExists = require('command-exists')\nconst os = require('os')\n\nmodule.exports = {\n  key: 'pandoc',\n  title: 'Pandoc',\n  description: 'Convert between markup formats. Required for converting from other formats such as MediaWiki, AsciiDoc, Textile and other wikis.',\n  async isCompatible () {\n    return os.arch() === 'x64'\n  },\n  isInstalled: false,\n  async check () {\n    try {\n      await cmdExists('pandoc')\n      this.isInstalled = true\n    } catch (err) {\n      this.isInstalled = false\n    }\n    return this.isInstalled\n  }\n}\n"
  },
  {
    "path": "server/modules/extensions/puppeteer/ext.js",
    "content": "const cmdExists = require('command-exists')\nconst os = require('os')\n\nmodule.exports = {\n  key: 'puppeteer',\n  title: 'Puppeteer',\n  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)',\n  async isCompatible () {\n    return os.arch() === 'x64'\n  },\n  isInstalled: false,\n  async check () {\n    try {\n      await cmdExists('pandoc')\n      this.isInstalled = true\n    } catch (err) {\n      this.isInstalled = false\n    }\n    return this.isInstalled\n  }\n}\n"
  },
  {
    "path": "server/modules/extensions/sharp/ext.js",
    "content": "const fs = require('fs-extra')\nconst os = require('os')\nconst path = require('path')\n\n/* global WIKI */\n\nmodule.exports = {\n  key: 'sharp',\n  title: 'Sharp',\n  description: 'Process and transform images. Required to generate thumbnails of uploaded images and perform transformations.',\n  async isCompatible () {\n    return os.arch() === 'x64'\n  },\n  isInstalled: false,\n  async check () {\n    this.isInstalled = await fs.pathExists(path.join(WIKI.ROOTPATH, 'node_modules/sharp'))\n    return this.isInstalled\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/airbrake/definition.yml",
    "content": "key: airbrake\ntitle: Airbrake\ndescription: Airbrake is the leading exception reporting service, currently providing error monitoring for 50,000 applications with support for 18 programming languages.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/airbrake.svg\nwebsite: https://airbrake.io/\ndefaultLevel: warn\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/airbrake/logger.js",
    "content": "// ------------------------------------\n// Airbrake\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/bugsnag/definition.yml",
    "content": "key: bugsnag\ntitle: Bugsnag\ndescription: Bugsnag monitors apps for errors that impact customers & reports all diagnostic data.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/bugsnag.svg\nwebsite: https://www.bugsnag.com/\ndefaultLevel: warn\nprops:\n  key:\n    type: String\n    title: Key\n    hint: Bugsnag Project Notifier key\n"
  },
  {
    "path": "server/modules/logging/bugsnag/logger.js",
    "content": "const util = require('util')\nconst winston = require('winston')\nconst _ = require('lodash')\n\n// ------------------------------------\n// Bugsnag\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n    let BugsnagLogger = winston.transports.BugsnagLogger = function (options) {\n      this.name = 'bugsnagLogger'\n      this.level = options.level || 'warn'\n      this.bugsnag = require('bugsnag')\n      this.bugsnag.register(options.key)\n    }\n    util.inherits(BugsnagLogger, winston.Transport)\n\n    BugsnagLogger.prototype.log = function (level, msg, meta, callback) {\n      this.bugsnag.notify(new Error(msg), _.assignIn(meta, { severity: level }))\n      callback(null, true)\n    }\n\n    logger.add(new BugsnagLogger({\n      level: 'warn',\n      key: conf.key\n    }))\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/disk/definition.yml",
    "content": "key: disk\ntitle: Log Files\ndescription: Outputs log files on local disk.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/local-fs.svg\nwebsite: https://wiki.js.org\ndefaultLevel: info\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/disk/logger.js",
    "content": "// ------------------------------------\n// Disk\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/eventlog/definition.yml",
    "content": "key: eventlog\ntitle: Windows Event Log\ndescription: Report logs to the Windows Event Log\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/windows-server.svg\nwebsite: https://wiki.js.org\ndefaultLevel: warn\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/eventlog/logger.js",
    "content": "// ------------------------------------\n// Windows Event Log\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/loggly/definition.yml",
    "content": "key: loggly\ntitle: Loggly\ndescription: Log Analysis / Log Management by Loggly, the world's most popular log analysis & monitoring in the cloud.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/loggly.svg\nwebsite: https://www.loggly.com/\ndefaultLevel: warn\nprops:\n  token:\n    type: String\n    title: Token\n    hint: Loggly Token\n  subdomain:\n    type: String\n    title: Subdomain\n    hint: Loggly Subdomain\n"
  },
  {
    "path": "server/modules/logging/loggly/logger.js",
    "content": "const winston = require('winston')\n\n// ------------------------------------\n// Loggly\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n    require('winston-loggly-bulk')\n    logger.add(new winston.transports.Loggly({\n      token: conf.token,\n      subdomain: conf.subdomain,\n      tags: ['wiki-js'],\n      level: 'warn',\n      json: true\n    }))\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/logstash/definition.yml",
    "content": "key: logstash\ntitle: Logstash\ndescription: Logstash is an open source tool for collecting, parsing, and storing logs for future use.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/logstash.svg\nwebsite: https://www.elastic.co/products/logstash\ndefaultLevel: warn\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/logstash/logger.js",
    "content": "// ------------------------------------\n// Logstash\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/newrelic/definition.yml",
    "content": "key: newrelic\ntitle: New Relic\ndescription: New Relic's digital intelligence platform lets developers, ops, and tech teams measure and monitor the performance of their applications and infrastructure.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/newrelic.svg\nwebsite: https://newrelic.com/\ndefaultLevel: warn\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/newrelic/logger.js",
    "content": "// ------------------------------------\n// New Relic\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/papertrail/definition.yml",
    "content": "key: papertrail\ntitle: Papertrail\ndescription: Frustration-free log management.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/papertrail.svg\nwebsite: https://papertrailapp.com/\ndefaultLevel: warn\nprops:\n  host:\n    type: String\n    title: Host\n  port:\n    type: Number\n    title: Port\n"
  },
  {
    "path": "server/modules/logging/papertrail/logger.js",
    "content": "const winston = require('winston')\n\n// ------------------------------------\n// Papertrail\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n    // eslint-disable-next-line no-unused-expressions\n    require('winston-papertrail').Papertrail // NOSONAR\n    logger.add(new winston.transports.Papertrail({\n      host: conf.host,\n      port: conf.port,\n      level: 'warn',\n      program: 'wiki.js'\n    }))\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/raygun/definition.yml",
    "content": "key: raygun\ntitle: Raygun\ndescription: Error, crash and performance monitoring for software teams.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/raygun.svg\nwebsite: https://raygun.com/\ndefaultLevel: warn\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/raygun/logger.js",
    "content": "// ------------------------------------\n// Raygun\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/rollbar/definition.yml",
    "content": "key: rollbar\ntitle: Rollbar\ndescription: Rollbar provides real-time error alerting & debugging tools for developers.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/rollbar.svg\nwebsite: https://rollbar.com/\ndefaultLevel: warn\nprops:\n  key:\n    type: String\n    title: Key\n"
  },
  {
    "path": "server/modules/logging/rollbar/logger.js",
    "content": "const util = require('util')\nconst winston = require('winston')\nconst _ = require('lodash')\n\n// ------------------------------------\n// Rollbar\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n    let RollbarLogger = winston.transports.RollbarLogger = function (options) {\n      this.name = 'rollbarLogger'\n      this.level = options.level || 'warn'\n      this.rollbar = require('rollbar')\n      this.rollbar.init(options.key)\n    }\n    util.inherits(RollbarLogger, winston.Transport)\n\n    RollbarLogger.prototype.log = function (level, msg, meta, callback) {\n      this.rollbar.handleErrorWithPayloadData(new Error(msg), _.assignIn(meta, { level }))\n      callback(null, true)\n    }\n\n    logger.add(new RollbarLogger({\n      level: 'warn',\n      key: conf.key\n    }))\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/sentry/definition.yml",
    "content": "key: sentry\ntitle: Sentry\ndescription: Open-source error tracking that helps developers monitor and fix crashes in real time.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/sentry.svg\nwebsite: https://sentry.io/\ndefaultLevel: warn\nprops:\n  key:\n    type: String\n    title: Key\n"
  },
  {
    "path": "server/modules/logging/sentry/logger.js",
    "content": "const util = require('util')\nconst winston = require('winston')\n\n// ------------------------------------\n// Sentry\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n    let SentryLogger = winston.transports.SentryLogger = function (options) {\n      this.name = 'sentryLogger'\n      this.level = options.level || 'warn'\n      this.raven = require('raven')\n      this.raven.config(options.key).install()\n    }\n    util.inherits(SentryLogger, winston.Transport)\n\n    SentryLogger.prototype.log = function (level, msg, meta, callback) {\n      level = (level === 'warn') ? 'warning' : level\n      this.raven.captureMessage(msg, { level, extra: meta })\n      callback(null, true)\n    }\n\n    logger.add(new SentryLogger({\n      level: 'warn',\n      key: conf.key\n    }))\n  }\n}\n"
  },
  {
    "path": "server/modules/logging/syslog/definition.yml",
    "content": "key: syslog\ntitle: Syslog\ndescription: Syslog is a way for network devices to send event messages to a logging server.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/syslog.svg\nwebsite: https://wiki.js.org\ndefaultLevel: warn\nprops: {}\n"
  },
  {
    "path": "server/modules/logging/syslog/logger.js",
    "content": "// ------------------------------------\n// Syslog\n// ------------------------------------\n\nmodule.exports = {\n  init (logger, conf) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/asciidoc-core/definition.yml",
    "content": "key: asciidocCore\ntitle: Core\ndescription: Basic Asciidoc Parser\nauthor: dzruyk (Based on asciidoctor.js renderer)\ninput: asciidoc\noutput: html\nicon: mdi-sitemap\nenabledDefault: true\nprops:\n  safeMode:\n    type: String\n    default: server\n    title: Safe Mode\n    hint: Sets the safe mode to use when parsing content to HTML.\n    order: 1\n    enum:\n      - unsafe\n      - safe\n      - server\n      - secure\n"
  },
  {
    "path": "server/modules/rendering/asciidoc-core/renderer.js",
    "content": "const asciidoctor = require('asciidoctor')()\nconst cheerio = require('cheerio')\n\nmodule.exports = {\n  async render() {\n    const html = asciidoctor.convert(this.input, {\n      standalone: false,\n      safe: this.config.safeMode,\n      attributes: {\n        showtitle: true,\n        icons: 'font'\n      }\n    })\n\n    const $ = cheerio.load(html, {\n      decodeEntities: true\n    })\n\n    $('pre.highlight > code.language-diagram').each((i, elm) => {\n      const diagramContent = Buffer.from($(elm).html(), 'base64').toString()\n      $(elm).parent().replaceWith(`<pre class=\"diagram\">${diagramContent}</div>`)\n    })\n\n    return $.html()\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-asciinema/definition.yml",
    "content": "key: htmlAsciinema\ntitle: Asciinema\ndescription: Embed asciinema players from compatible links\nauthor: requarks.io\nicon: mdi-theater\nenabledDefault: false\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-asciinema/renderer.js",
    "content": "module.exports = {\n  init($, config) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-blockquotes/definition.yml",
    "content": "key: htmlBlockquotes\ntitle: Blockquotes\ndescription: Parse blockquotes box styling\nauthor: requarks.io\nicon: mdi-alpha-t-box-outline\nenabledDefault: true\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-blockquotes/renderer.js",
    "content": "module.exports = {\n  init($, config) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-codehighlighter/definition.yml",
    "content": "key: htmlCodehighlighter\ntitle: Code Highlighting Post-Processor\ndescription: Syntax detector for programming code\nauthor: requarks.io\nicon: mdi-code-braces\nenabledDefault: true\ndependsOn: htmlCore\nstep: pre\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-codehighlighter/renderer.js",
    "content": "const hljs = require('highlight.js')\n\nmodule.exports = {\n  async init($, config) {\n    $('pre > code').each((idx, elm) => {\n      const codeClasses = $(elm).attr('class') || ''\n      if (codeClasses.indexOf('language-') < 0) {\n        const result = hljs.highlightAuto($(elm).text())\n        $(elm).addClass('language-', result.language)\n      }\n      $(elm).parent().addClass('prismjs line-numbers')\n    })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-core/definition.yml",
    "content": "key: htmlCore\ntitle: Core\ndescription: Basic HTML Parser\nauthor: requarks.io\ninput: html\noutput: html\nicon: mdi-language-html5\nprops:\n  absoluteLinks:\n    type: Boolean\n    default: false\n    title: Treat relative links as root absolute\n    hint: For example, a link to foo/bar on page xyz will render as /foo/bar instead of /xyz/foo/bar.\n    order: 1\n  openExternalLinkNewTab:\n    type: Boolean\n    default: false\n    title: Open external links in a new tab\n    hint: External links will have a _blank target attribute added automatically.\n    order: 2\n  relAttributeExternalLink:\n    type: String\n    default: noreferrer\n    title: Protect against XSS when opening _blank target links\n    hint: External links with _blank attribute will have an additional rel attribute.\n    order: 3\n    enum:\n        - noreferrer\n        - noopener\n"
  },
  {
    "path": "server/modules/rendering/html-core/renderer.js",
    "content": "const _ = require('lodash')\nconst cheerio = require('cheerio')\nconst uslug = require('uslug')\nconst pageHelper = require('../../../helpers/page')\nconst URL = require('url').URL\n\nconst mustacheRegExp = /(\\{|&#x7b;?){2}(.+?)(\\}|&#x7d;?){2}/i\n\n/* global WIKI */\n\nmodule.exports = {\n  async render() {\n    let $ = cheerio.load(this.input, {\n      decodeEntities: true\n    })\n\n    if ($.root().children().length < 1) {\n      return ''\n    }\n\n    // --------------------------------\n    // STEP: PRE\n    // --------------------------------\n\n    for (let child of _.reject(this.children, ['step', 'post'])) {\n      const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)\n      await renderer.init($, child.config)\n    }\n\n    // --------------------------------\n    // Detect internal / external links\n    // --------------------------------\n\n    let internalRefs = []\n    const reservedPrefixes = /^\\/[a-z]\\//i\n    const exactReservedPaths = /^\\/[a-z]$/i\n\n    const isHostSet = WIKI.config.host.length > 7 && WIKI.config.host !== 'http://'\n    if (!isHostSet) {\n      WIKI.logger.warn('Host is not set. You must set the Site Host under General in the Administration Area!')\n    }\n\n    $('a').each((i, elm) => {\n      let href = $(elm).attr('href')\n\n      // -> Ignore empty / anchor links, e-mail addresses, and telephone numbers\n      if (!href || href.length < 1 || href.indexOf('#') === 0 ||\n        href.indexOf('mailto:') === 0 || href.indexOf('tel:') === 0) {\n        return\n      }\n\n      // -> Strip host from local links\n      if (isHostSet && href.indexOf(`${WIKI.config.host}/`) === 0) {\n        href = href.replace(WIKI.config.host, '')\n      }\n\n      // -> Assign local / external tag\n      if (href.indexOf('://') < 0) {\n        // -> Remove trailing slash\n        if (_.endsWith('/')) {\n          href = href.slice(0, -1)\n        }\n\n        // -> Check for system prefix\n        if (reservedPrefixes.test(href) || exactReservedPaths.test(href)) {\n          $(elm).addClass(`is-system-link`)\n        } else if (href.indexOf('.') >= 0) {\n          $(elm).addClass(`is-asset-link`)\n        } else {\n          let pagePath = null\n\n          // -> Add locale prefix if using namespacing\n          if (WIKI.config.lang.namespacing) {\n            // -> Reformat paths\n            if (href.indexOf('/') !== 0) {\n              if (this.config.absoluteLinks) {\n                href = `/${this.page.localeCode}/${href}`\n              } else {\n                href = (this.page.path === 'home') ? `/${this.page.localeCode}/${href}` : `/${this.page.localeCode}/${this.page.path}/${href}`\n              }\n            } else if (href.charAt(3) !== '/') {\n              href = `/${this.page.localeCode}${href}`\n            }\n\n            try {\n              const parsedUrl = new URL(`http://x${href}`)\n              pagePath = pageHelper.parsePath(parsedUrl.pathname)\n            } catch (err) {\n              return\n            }\n          } else {\n            // -> Reformat paths\n            if (href.indexOf('/') !== 0) {\n              if (this.config.absoluteLinks) {\n                href = `/${href}`\n              } else {\n                href = (this.page.path === 'home') ? `/${href}` : `/${this.page.path}/${href}`\n              }\n            }\n\n            try {\n              const parsedUrl = new URL(`http://x${href}`)\n              pagePath = pageHelper.parsePath(parsedUrl.pathname)\n            } catch (err) {\n              return\n            }\n          }\n          // -> Save internal references\n          internalRefs.push({\n            localeCode: pagePath.locale,\n            path: pagePath.path\n          })\n\n          $(elm).addClass(`is-internal-link`)\n        }\n      } else {\n        $(elm).addClass(`is-external-link`)\n        if (this.config.openExternalLinkNewTab) {\n          $(elm).attr('target', '_blank')\n          $(elm).attr('rel', this.config.relAttributeExternalLink)\n        }\n      }\n\n      // -> Update element\n      $(elm).attr('href', href)\n    })\n\n    // --------------------------------\n    // Detect internal link states\n    // --------------------------------\n\n    const pastLinks = await this.page.$relatedQuery('links')\n\n    if (internalRefs.length > 0) {\n      // -> Find matching pages\n      const results = await WIKI.models.pages.query().column('id', 'path', 'localeCode').where(builder => {\n        internalRefs.forEach((ref, idx) => {\n          if (idx < 1) {\n            builder.where(ref)\n          } else {\n            builder.orWhere(ref)\n          }\n        })\n      })\n\n      // -> Apply tag to internal links for found pages\n      $('a.is-internal-link').each((i, elm) => {\n        const href = $(elm).attr('href')\n        let hrefObj = {}\n        try {\n          const parsedUrl = new URL(`http://x${href}`)\n          hrefObj = pageHelper.parsePath(parsedUrl.pathname)\n        } catch (err) {\n          return\n        }\n        if (_.some(results, r => {\n          return r.localeCode === hrefObj.locale && r.path === hrefObj.path\n        })) {\n          $(elm).addClass(`is-valid-page`)\n        } else {\n          $(elm).addClass(`is-invalid-page`)\n        }\n      })\n\n      // -> Add missing links\n      const missingLinks = _.differenceWith(internalRefs, pastLinks, (nLink, pLink) => {\n        return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path\n      })\n      if (missingLinks.length > 0) {\n        if (WIKI.config.db.type === 'postgres') {\n          await WIKI.models.pageLinks.query().insert(missingLinks.map(lnk => ({\n            pageId: this.page.id,\n            path: lnk.path,\n            localeCode: lnk.localeCode\n          })))\n        } else {\n          for (const lnk of missingLinks) {\n            await WIKI.models.pageLinks.query().insert({\n              pageId: this.page.id,\n              path: lnk.path,\n              localeCode: lnk.localeCode\n            })\n          }\n        }\n      }\n    }\n\n    // -> Remove outdated links\n    if (pastLinks) {\n      const outdatedLinks = _.differenceWith(pastLinks, internalRefs, (nLink, pLink) => {\n        return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path\n      })\n      if (outdatedLinks.length > 0) {\n        await WIKI.models.pageLinks.query().delete().whereIn('id', _.map(outdatedLinks, 'id'))\n      }\n    }\n\n    // --------------------------------\n    // Add header handles\n    // --------------------------------\n\n    let headers = []\n    $('h1,h2,h3,h4,h5,h6').each((i, elm) => {\n      let headerSlug = uslug($(elm).text())\n      // -> If custom ID is defined, try to use that instead\n      if ($(elm).attr('id')) {\n        headerSlug = $(elm).attr('id')\n      }\n\n      // -> Cannot start with a number (CSS selector limitation)\n      if (headerSlug.match(/^\\d/)) {\n        headerSlug = `h-${headerSlug}`\n      }\n\n      // -> Make sure header is unique\n      if (headers.indexOf(headerSlug) >= 0) {\n        let isUnique = false\n        let hIdx = 1\n        while (!isUnique) {\n          const headerSlugTry = `${headerSlug}-${hIdx}`\n          if (headers.indexOf(headerSlugTry) < 0) {\n            isUnique = true\n            headerSlug = headerSlugTry\n          }\n          hIdx++\n        }\n      }\n\n      // -> Add anchor\n      $(elm).attr('id', headerSlug).addClass('toc-header')\n      $(elm).prepend(`<a class=\"toc-anchor\" href=\"#${headerSlug}\">&#xB6;</a> `)\n\n      headers.push(headerSlug)\n    })\n\n    // --------------------------------\n    // Wrap non-empty root text nodes\n    // --------------------------------\n\n    $('body').contents().toArray().forEach(item => {\n      if (item && item.type === 'text' && item.parent.name === 'body' && item.data !== `\\n` && item.data !== `\\r`) {\n        $(item).wrap('<div></div>')\n      }\n    })\n\n    // --------------------------------\n    // Wrap root table nodes\n    // --------------------------------\n\n    $('body').contents().toArray().forEach(item => {\n      if (item && item.name === 'table' && item.parent.name === 'body') {\n        $(item).wrap('<div class=\"table-container\"></div>')\n      }\n    })\n\n    // --------------------------------\n    // STEP: POST\n    // --------------------------------\n\n    let output = decodeEscape($.html('body').replace('<body>', '').replace('</body>', ''))\n\n    for (let child of _.sortBy(_.filter(this.children, ['step', 'post']), ['order'])) {\n      const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)\n      output = await renderer.init(output, child.config)\n    }\n\n    // --------------------------------\n    // Escape mustache expresions\n    // --------------------------------\n\n    $ = cheerio.load(output, {\n      decodeEntities: true\n    })\n\n    function iterateMustacheNode (node) {\n      $(node).contents().each((idx, item) => {\n        if (item && item.type === 'text') {\n          const rawText = $(item).text().replace(/\\r?\\n|\\r/g, '')\n          if (mustacheRegExp.test(rawText)) {\n            if (!item.parent || item.parent.name === 'body') {\n              $(item).wrap($('<p>').attr('v-pre', true))\n            } else {\n              $(item).parent().attr('v-pre', true)\n            }\n          }\n        } else {\n          iterateMustacheNode(item)\n        }\n      })\n    }\n    iterateMustacheNode($.root())\n\n    $('pre').each((idx, elm) => {\n      $(elm).attr('v-pre', true)\n    })\n\n    return decodeEscape($.html('body').replace('<body>', '').replace('</body>', ''))\n  }\n}\n\nfunction decodeEscape (string) {\n  return string.replace(/&#x([0-9a-f]{1,6});/ig, (entity, code) => {\n    code = parseInt(code, 16)\n\n    // Don't unescape ASCII characters, assuming they're encoded for a good reason\n    if (code < 0x80) return entity\n\n    return String.fromCodePoint(code)\n  })\n}\n"
  },
  {
    "path": "server/modules/rendering/html-diagram/definition.yml",
    "content": "key: htmlDiagram\ntitle: Diagrams Post-Processor\ndescription: HTML Processing for diagrams (draw.io)\nauthor: requarks.io\nicon: mdi-chart-multiline\nenabledDefault: true\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-diagram/renderer.js",
    "content": "module.exports = {\n  async init($, config) {\n    $(`pre.diagram`).each((idx, elm) => {\n      $(elm).children('svg').each((sidx, svg) => {\n        $(svg).removeAttr('content')\n      })\n      $(elm).replaceWith($(`<div class=\"diagram\">${$(elm).html()}</div>`))\n    })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-image-prefetch/definition.yml",
    "content": "key: htmlImagePrefetch\ntitle: Image Prefetch\ndescription: Prefetch remotely rendered images (kroki/plantuml)\nauthor: requarks.io\nicon: mdi-cloud-download-outline\nenabledDefault: false\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-image-prefetch/renderer.js",
    "content": "const request = require('request-promise')\n\nconst prefetch = async (element) => {\n  const url = element.attr(`src`)\n  let response\n  try {\n    response = await request({\n      method: `GET`,\n      url,\n      resolveWithFullResponse: true\n    })\n  } catch (err) {\n    WIKI.logger.warn(`Failed to prefetch ${url}`)\n    WIKI.logger.warn(err)\n    return\n  }\n  const contentType = response.headers[`content-type`]\n  const image = Buffer.from(response.body).toString('base64')\n  element.attr('src', `data:${contentType};base64,${image}`)\n  element.removeClass('prefetch-candidate')\n}\n\nmodule.exports = {\n  async init($) {\n    const promises = $('img.prefetch-candidate').map((index, element) => {\n      return prefetch($(element))\n    }).toArray()\n    await Promise.all(promises)\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-mediaplayers/definition.yml",
    "content": "key: htmlMediaplayers\ntitle: Media Players\ndescription: Embed players such as Youtube, Vimeo, Soundcloud, etc.\nauthor: requarks.io\nicon: mdi-video\nenabledDefault: true\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-mediaplayers/renderer.js",
    "content": "module.exports = {\n  init($, config) {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-mermaid/definition.yml",
    "content": "key: htmlMermaid\ntitle: Mermaid\ndescription: Generate flowcharts from Mermaid syntax\nauthor: requarks.io\nicon: mdi-arrow-decision-outline\nenabledDefault: true\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-mermaid/renderer.js",
    "content": "module.exports = {\n  init($, config) {\n    $('pre.prismjs > code.language-mermaid').each((i, elm) => {\n      const mermaidContent = $(elm).html()\n      $(elm).parent().replaceWith(`<div class=\"mermaid\">${mermaidContent}</div>`)\n    })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-security/definition.yml",
    "content": "key: htmlSecurity\ntitle: Security\ndescription: Filter and strips potentially dangerous content\nauthor: requarks.io\nicon: mdi-fire\nenabledDefault: true\ndependsOn: htmlCore\nstep: post\norder: 99999\nprops:\n  safeHTML:\n    type: Boolean\n    title: Sanitize HTML\n    default: true\n    hint: Sanitize HTML from unsafe attributes and tags that could lead to XSS attacks\n    order: 1\n  allowDrawIoUnsafe:\n    type: Boolean\n    title: Allow Draw.io Unsafe Elements\n    default: true\n    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.\n    order: 2\n  allowIFrames:\n    type: Boolean\n    title: Allow iframes\n    default: false\n    hint: iframes will not be stripped if enabled. (Not recommended)\n    order: 3\n"
  },
  {
    "path": "server/modules/rendering/html-security/renderer.js",
    "content": "const { JSDOM } = require('jsdom')\nconst createDOMPurify = require('dompurify')\n\nmodule.exports = {\n  async init(input, config) {\n    if (config.safeHTML) {\n      const window = new JSDOM('').window\n      const DOMPurify = createDOMPurify(window)\n\n      const allowedAttrs = ['v-pre', 'v-slot:tabs', 'v-slot:content', 'target']\n      const allowedTags = ['tabset', 'template']\n\n      if (config.allowDrawIoUnsafe) {\n        allowedTags.push('foreignObject')\n        DOMPurify.addHook('uponSanitizeElement', (elm) => {\n          if (elm.querySelectorAll) {\n            const breaks = elm.querySelectorAll('foreignObject br, foreignObject p')\n            if (breaks && breaks.length) {\n              for (let i = 0; i < breaks.length; i++) {\n                breaks[i].parentNode.replaceChild(\n                  window.document.createElement('div'),\n                  breaks[i]\n                )\n              }\n            }\n          }\n        })\n      }\n\n      if (config.allowIFrames) {\n        allowedTags.push('iframe')\n        allowedAttrs.push('allow')\n      }\n\n      input = DOMPurify.sanitize(input, {\n        ADD_ATTR: allowedAttrs,\n        ADD_TAGS: allowedTags,\n        HTML_INTEGRATION_POINTS: { foreignobject: true }\n      })\n    }\n    return input\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-tabset/definition.yml",
    "content": "key: htmlTabset\ntitle: Tabsets\ndescription: Transform headers into tabs\nauthor: requarks.io\nicon: mdi-tab\nenabledDefault: true\ndependsOn: htmlCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-tabset/renderer.js",
    "content": "const _ = require('lodash')\n\nmodule.exports = {\n  async init($, config) {\n    for (let i = 1; i < 6; i++) {\n      $(`h${i}.tabset`).each((idx, elm) => {\n        let content = `<tabset>`\n        let tabs = []\n        let tabContents = []\n        $(elm).nextUntil(_.times(i, t => `h${t + 1}`).join(', '), `h${i + 1}`).each((hidx, hd) => {\n          tabs.push(`<li>${$(hd).html()}</li>`)\n          let tabContent = ''\n          $(hd).nextUntil(_.times(i + 1, t => `h${t + 1}`).join(', ')).each((cidx, celm) => {\n            tabContent += $.html(celm)\n            $(celm).remove()\n          })\n          tabContents.push(`<div class=\"tabset-panel\">${tabContent}</div>`)\n          $(hd).remove()\n        })\n        content += `<template v-slot:tabs>${tabs.join('')}</template>`\n        content += `<template v-slot:content>${tabContents.join('')}</template>`\n        content += `</tabset>`\n        $(elm).replaceWith($(content))\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/html-twemoji/definition.yml",
    "content": "key: htmlTwemoji\ntitle: Twemoji\ndescription: Apply Twitter Emojis to all Unicode emojis\nauthor: requarks.io\nicon: mdi-emoticon-happy-outline\nenabledDefault: true\ndependsOn: htmlCore\nstep: post\norder: 10\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/html-twemoji/renderer.js",
    "content": "// const twemoji = require('twemoji')\n\n// ------------------------------------\n// HTML - Twemoji\n// ------------------------------------\n\nmodule.exports = {\n  init (input, conf) {\n    // TODO: Must limit to text nodes only (exclude code blocks, already processed emojis, etc.)\n    //\n    // return twemoji.parse(input)\n    return input\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-abbr/definition.yml",
    "content": "key: markdownAbbr\ntitle: Abbreviations\ndescription: Parse abbreviations into abbr tags\nauthor: requarks.io\nicon: mdi-contain-start\nenabledDefault: true\ndependsOn: markdownCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-abbr/renderer.js",
    "content": "const mdAbbr = require('markdown-it-abbr')\n\n// ------------------------------------\n// Markdown - Abbreviations\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(mdAbbr)\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-core/definition.yml",
    "content": "key: markdownCore\ntitle: Core\ndescription: Basic Markdown Parser\nauthor: requarks.io\ninput: markdown\noutput: html\nicon: mdi-language-markdown\nprops:\n  allowHTML:\n    type: Boolean\n    default: true\n    title: Allow HTML\n    hint: Enable HTML tags in content.\n    order: 1\n    public: true\n  linkify:\n    type: Boolean\n    default: true\n    title: Automatically convert links\n    hint: Links will automatically be converted to clickable links.\n    order: 2\n    public: true\n  linebreaks:\n    type: Boolean\n    default: true\n    title: Automatically convert line breaks\n    hint: Add linebreaks within paragraphs.\n    order: 3\n    public: true\n  underline:\n    type: Boolean\n    default: false\n    title: Underline Emphasis\n    hint: Enable text underlining by using _underline_ syntax.\n    order: 4\n    public: true\n  typographer:\n    type: Boolean\n    default: false\n    title: Typographer\n    hint: Enable some language-neutral replacement + quotes beautification.\n    order: 5\n    public: true\n  quotes:\n    type: String\n    default: English\n    title: Quotes style\n    hint: When typographer is enabled. Double + single quotes replacement pairs. e.g. «»„“ for Russian, „“‚‘ for German, etc.\n    order: 6\n    enum:\n      - Chinese\n      - English\n      - French\n      - German\n      - Greek\n      - Japanese\n      - Hungarian\n      - Polish\n      - Portuguese\n      - Russian\n      - Spanish\n      - Swedish\n    public: true\n"
  },
  {
    "path": "server/modules/rendering/markdown-core/renderer.js",
    "content": "const md = require('markdown-it')\nconst mdAttrs = require('markdown-it-attrs')\nconst mdDecorate = require('markdown-it-decorate')\nconst _ = require('lodash')\nconst underline = require('./underline')\n\nconst quoteStyles = {\n  Chinese: '””‘’',\n  English: '“”‘’',\n  French: ['«\\xA0', '\\xA0»', '‹\\xA0', '\\xA0›'],\n  German: '„“‚‘',\n  Greek: '«»‘’',\n  Japanese: '「」「」',\n  Hungarian: '„”’’',\n  Polish: '„”‚‘',\n  Portuguese: '«»‘’',\n  Russian: '«»„“',\n  Spanish: '«»‘’',\n  Swedish: '””’’'\n}\n\nmodule.exports = {\n  async render() {\n    const mkdown = md({\n      html: this.config.allowHTML,\n      breaks: this.config.linebreaks,\n      linkify: this.config.linkify,\n      typographer: this.config.typographer,\n      quotes: _.get(quoteStyles, this.config.quotes, quoteStyles.English),\n      highlight(str, lang) {\n        if (lang === 'diagram') {\n          return `<pre class=\"diagram\">` + Buffer.from(str, 'base64').toString() + `</pre>`\n        } else {\n          return `<pre><code class=\"language-${lang}\">${_.escape(str)}</code></pre>`\n        }\n      }\n    })\n\n    if (this.config.underline) {\n      mkdown.use(underline)\n    }\n\n    mkdown.use(mdAttrs, {\n      allowedAttributes: ['id', 'class', 'target']\n    })\n    mkdown.use(mdDecorate)\n\n    for (let child of this.children) {\n      const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)\n      await renderer.init(mkdown, child.config)\n    }\n\n    return mkdown.render(this.input)\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-core/underline.js",
    "content": "const renderEm = (tokens, idx, opts, env, slf) => {\n  const token = tokens[idx]\n  if (token.markup === '_') {\n    token.tag = 'u'\n  }\n  return slf.renderToken(tokens, idx, opts)\n}\n\nmodule.exports = (md) => {\n  md.renderer.rules.em_open = renderEm\n  md.renderer.rules.em_close = renderEm\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-emoji/definition.yml",
    "content": "key: markdownEmoji\ntitle: Emoji\ndescription: Convert tags to emojis\nauthor: requarks.io\nicon: mdi-sticker-emoji\nenabledDefault: true\ndependsOn: markdownCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-emoji/renderer.js",
    "content": "const { full: mdEmoji } = require('markdown-it-emoji')\nconst twemoji = require('twemoji')\n\n// ------------------------------------\n// Markdown - Emoji\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(mdEmoji)\n\n    md.renderer.rules.emoji = (token, idx) => {\n      return twemoji.parse(token[idx].content, {\n        callback (icon, opts) {\n          return `/_assets/svg/twemoji/${icon}.svg`\n        }\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-expandtabs/definition.yml",
    "content": "key: markdownExpandtabs\ntitle: Expand Tabs\ndescription: Replace tabs with spaces in code blocks\nauthor: requarks.io\nicon: mdi-arrow-expand-horizontal\nenabledDefault: true\ndependsOn: markdownCore\nprops:\n  tabWidth:\n    type: Number\n    title: Tab Width\n    hint: Amount of spaces for each tab\n    default: 4\n"
  },
  {
    "path": "server/modules/rendering/markdown-expandtabs/renderer.js",
    "content": "const mdExpandTabs = require('markdown-it-expand-tabs')\nconst _ = require('lodash')\n\n// ------------------------------------\n// Markdown - Expand Tabs\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(mdExpandTabs, {\n      tabWidth: _.toInteger(conf.tabWidth || 4)\n    })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-footnotes/definition.yml",
    "content": "key: markdownFootnotes\ntitle: Footnotes\ndescription: Parse footnotes references\nauthor: requarks.io\nicon: mdi-page-layout-footer\nenabledDefault: true\ndependsOn: markdownCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-footnotes/renderer.js",
    "content": "const mdFootnote = require('markdown-it-footnote')\n\n// ------------------------------------\n// Markdown - Footnotes\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(mdFootnote)\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-imsize/definition.yml",
    "content": "key: markdownImsize\ntitle: Image Size\ndescription: Adds dimensions attributes to images\nauthor: requarks.io\nicon: mdi-image-size-select-large\nenabledDefault: true\ndependsOn: markdownCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-imsize/renderer.js",
    "content": "const mdImsize = require('markdown-it-imsize')\n\n// ------------------------------------\n// Markdown - Image Size\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(mdImsize)\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-katex/definition.yml",
    "content": "key: markdownKatex\ntitle: Katex\ndescription: LaTeX Math + Chemical Expression Typesetting Renderer\nauthor: requarks.io\nicon: mdi-math-integral\nenabledDefault: true\ndependsOn: markdownCore\nprops:\n  useInline:\n    type: Boolean\n    default: true\n    title: Inline TeX\n    hint: Process inline TeX expressions surrounded by $ symbols.\n    order: 1\n  useBlocks:\n    type: Boolean\n    default: true\n    title: TeX Blocks\n    hint: Process TeX blocks enclosed by $$ symbols.\n    order: 2\n"
  },
  {
    "path": "server/modules/rendering/markdown-katex/mhchem.js",
    "content": "/* eslint-disable */\n/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */\n/* vim: set ts=2 et sw=2 tw=80: */\n\n/*************************************************************\n *\n *  KaTeX mhchem.js\n *\n *  This file implements a KaTeX version of mhchem version 3.3.0.\n *  It is adapted from MathJax/extensions/TeX/mhchem.js\n *  It differs from the MathJax version as follows:\n *    1. The interface is changed so that it can be called from KaTeX, not MathJax.\n *    2. \\rlap and \\llap are replaced with \\mathrlap and \\mathllap.\n *    3. Four lines of code are edited in order to use \\raisebox instead of \\raise.\n *    4. The reaction arrow code is simplified. All reaction arrows are rendered\n *       using KaTeX extensible arrows instead of building non-extensible arrows.\n *    5. \\tripledash vertical alignment is slightly adjusted.\n *\n *    This code, as other KaTeX code, is released under the MIT license.\n *\n * /*************************************************************\n *\n *  MathJax/extensions/TeX/mhchem.js\n *\n *  Implements the \\ce command for handling chemical formulas\n *  from the mhchem LaTeX package.\n *\n *  ---------------------------------------------------------------------\n *\n *  Copyright (c) 2011-2015 The MathJax Consortium\n *  Copyright (c) 2015-2018 Martin Hensel\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\n//\n// Coding Style\n//   - use '' for identifiers that can by minified/uglified\n//   - use \"\" for strings that need to stay untouched\n\n// version: \"3.3.0\" for MathJax and KaTeX\n\n\n  //\n  //  This is the main function for handing the \\ce and \\pu commands.\n  //  It takes the argument to \\ce or \\pu and returns the corresponding TeX string.\n  //\n\n  module.exports = function (tokens, stateMachine) {\n    // Recreate the argument string from KaTeX's array of tokens.\n    var str = \"\";\n    var expectedLoc = tokens[tokens.length - 1].loc.start\n    for (var i = tokens.length - 1; i >= 0; i--) {\n      if(tokens[i].loc.start > expectedLoc) {\n        // context.consumeArgs has eaten a space.\n        str += \" \";\n        expectedLoc = tokens[i].loc.start;\n      }\n      str += tokens[i].text;\n      expectedLoc += tokens[i].text.length;\n    }\n    var tex = texify.go(mhchemParser.go(str, stateMachine));\n    return tex;\n  };\n\n  //\n  // Core parser for mhchem syntax  (recursive)\n  //\n  /** @type {MhchemParser} */\n  var mhchemParser = {\n    //\n    // Parses mchem \\ce syntax\n    //\n    // Call like\n    //   go(\"H2O\");\n    //\n    go: function (input, stateMachine) {\n      if (!input) { return []; }\n      if (stateMachine === undefined) { stateMachine = 'ce'; }\n      var state = '0';\n\n      //\n      // String buffers for parsing:\n      //\n      // buffer.a == amount\n      // buffer.o == element\n      // buffer.b == left-side superscript\n      // buffer.p == left-side subscript\n      // buffer.q == right-side subscript\n      // buffer.d == right-side superscript\n      //\n      // buffer.r == arrow\n      // buffer.rdt == arrow, script above, type\n      // buffer.rd == arrow, script above, content\n      // buffer.rqt == arrow, script below, type\n      // buffer.rq == arrow, script below, content\n      //\n      // buffer.text_\n      // buffer.rm\n      // etc.\n      //\n      // buffer.parenthesisLevel == int, starting at 0\n      // buffer.sb == bool, space before\n      // buffer.beginsWithBond == bool\n      //\n      // These letters are also used as state names.\n      //\n      // Other states:\n      // 0 == begin of main part (arrow/operator unlikely)\n      // 1 == next entity\n      // 2 == next entity (arrow/operator unlikely)\n      // 3 == next atom\n      // c == macro\n      //\n      /** @type {Buffer} */\n      var buffer = {};\n      buffer['parenthesisLevel'] = 0;\n\n      input = input.replace(/\\n/g, \" \");\n      input = input.replace(/[\\u2212\\u2013\\u2014\\u2010]/g, \"-\");\n      input = input.replace(/[\\u2026]/g, \"...\");\n\n      //\n      // Looks through mhchemParser.transitions, to execute a matching action\n      // (recursive)\n      //\n      var lastInput;\n      var watchdog = 10;\n      /** @type {ParserOutput[]} */\n      var output = [];\n      while (true) {\n        if (lastInput !== input) {\n          watchdog = 10;\n          lastInput = input;\n        } else {\n          watchdog--;\n        }\n        //\n        // Find actions in transition table\n        //\n        var machine = mhchemParser.stateMachines[stateMachine];\n        var t = machine.transitions[state] || machine.transitions['*'];\n        iterateTransitions:\n        for (var i=0; i<t.length; i++) {\n          var matches = mhchemParser.patterns.match_(t[i].pattern, input);\n          if (matches) {\n            //\n            // Execute actions\n            //\n            var task = t[i].task;\n            for (var iA=0; iA<task.action_.length; iA++) {\n              var o;\n              //\n              // Find and execute action\n              //\n              if (machine.actions[task.action_[iA].type_]) {\n                o = machine.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option);\n              } else if (mhchemParser.actions[task.action_[iA].type_]) {\n                o = mhchemParser.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option);\n              } else {\n                throw [\"MhchemBugA\", \"mhchem bug A. Please report. (\" + task.action_[iA].type_ + \")\"];  // Trying to use non-existing action\n              }\n              //\n              // Add output\n              //\n              mhchemParser.concatArray(output, o);\n            }\n            //\n            // Set next state,\n            // Shorten input,\n            // Continue with next character\n            //   (= apply only one transition per position)\n            //\n            state = task.nextState || state;\n            if (input.length > 0) {\n              if (!task.revisit) {\n                input = matches.remainder;\n              }\n              if (!task.toContinue) {\n                break iterateTransitions;\n              }\n            } else {\n              return output;\n            }\n          }\n        }\n        //\n        // Prevent infinite loop\n        //\n        if (watchdog <= 0) {\n          throw [\"MhchemBugU\", \"mhchem bug U. Please report.\"];  // Unexpected character\n        }\n      }\n    },\n    concatArray: function (a, b) {\n      if (b) {\n        if (Array.isArray(b)) {\n          for (var iB=0; iB<b.length; iB++) {\n            a.push(b[iB]);\n          }\n        } else {\n          a.push(b);\n        }\n      }\n    },\n\n    patterns: {\n      //\n      // Matching patterns\n      // either regexps or function that return null or {match_:\"a\", remainder:\"bc\"}\n      //\n      patterns: {\n        // property names must not look like integers (\"2\") for correct property traversal order, later on\n        'empty': /^$/,\n        'else': /^./,\n        'else2': /^./,\n        'space': /^\\s/,\n        'space A': /^\\s(?=[A-Z\\\\$])/,\n        'space$': /^\\s$/,\n        'a-z': /^[a-z]/,\n        'x': /^x/,\n        'x$': /^x$/,\n        'i$': /^i$/,\n        'letters': /^(?:[a-zA-Z\\u03B1-\\u03C9\\u0391-\\u03A9?@]|(?:\\\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\\s+|\\{\\}|(?![a-zA-Z]))))+/,\n        '\\\\greek': /^\\\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\\s+|\\{\\}|(?![a-zA-Z]))/,\n        'one lowercase latin letter $': /^(?:([a-z])(?:$|[^a-zA-Z]))$/,\n        '$one lowercase latin letter$ $': /^\\$(?:([a-z])(?:$|[^a-zA-Z]))\\$$/,\n        'one lowercase greek letter $': /^(?:\\$?[\\u03B1-\\u03C9]\\$?|\\$?\\\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)\\s*\\$?)(?:\\s+|\\{\\}|(?![a-zA-Z]))$/,\n        'digits': /^[0-9]+/,\n        '-9.,9': /^[+\\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))/,\n        '-9.,9 no missing 0': /^[+\\-]?[0-9]+(?:[.,][0-9]+)?/,\n        '(-)(9.,9)(e)(99)': function (input) {\n          var m = input.match(/^(\\+\\-|\\+\\/\\-|\\+|\\-|\\\\pm\\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))?(\\((?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))\\))?(?:([eE]|\\s*(\\*|x|\\\\times|\\u00D7)\\s*10\\^)([+\\-]?[0-9]+|\\{[+\\-]?[0-9]+\\}))?/);\n          if (m && m[0]) {\n            return { match_: m.splice(1), remainder: input.substr(m[0].length) };\n          }\n          return null;\n        },\n        '(-)(9)^(-9)': function (input) {\n          var m = input.match(/^(\\+\\-|\\+\\/\\-|\\+|\\-|\\\\pm\\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+)?)\\^([+\\-]?[0-9]+|\\{[+\\-]?[0-9]+\\})/);\n          if (m && m[0]) {\n            return { match_: m.splice(1), remainder: input.substr(m[0].length) };\n          }\n          return null;\n        },\n        'state of aggregation $': function (input) {  // ... or crystal system\n          var a = mhchemParser.patterns.findObserveGroups(input, \"\", /^\\([a-z]{1,3}(?=[\\),])/, \")\", \"\");  // (aq), (aq,$\\infty$), (aq, sat)\n          if (a  &&  a.remainder.match(/^($|[\\s,;\\)\\]\\}])/)) { return a; }  //  AND end of 'phrase'\n          var m = input.match(/^(?:\\((?:\\\\ca\\s?)?\\$[amothc]\\$\\))/);  // OR crystal system ($o$) (\\ca$c$)\n          if (m) {\n            return { match_: m[0], remainder: input.substr(m[0].length) };\n          }\n          return null;\n        },\n        '_{(state of aggregation)}$': /^_\\{(\\([a-z]{1,3}\\))\\}/,\n        '{[(': /^(?:\\\\\\{|\\[|\\()/,\n        ')]}': /^(?:\\)|\\]|\\\\\\})/,\n        ', ': /^[,;]\\s*/,\n        ',': /^[,;]/,\n        '.': /^[.]/,\n        '. ': /^([.\\u22C5\\u00B7\\u2022])\\s*/,\n        '...': /^\\.\\.\\.(?=$|[^.])/,\n        '* ': /^([*])\\s*/,\n        '^{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"^{\", \"\", \"\", \"}\"); },\n        '^($...$)': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"^\", \"$\", \"$\", \"\"); },\n        '^a': /^\\^([0-9]+|[^\\\\_])/,\n        '^\\\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"^\", /^\\\\[a-zA-Z]+\\{/, \"}\", \"\", \"\", \"{\", \"}\", \"\", true); },\n        '^\\\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"^\", /^\\\\[a-zA-Z]+\\{/, \"}\", \"\"); },\n        '^\\\\x': /^\\^(\\\\[a-zA-Z]+)\\s*/,\n        '^(-1)': /^\\^(-?\\d+)/,\n        '\\'': /^'/,\n        '_{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"_{\", \"\", \"\", \"}\"); },\n        '_($...$)': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"_\", \"$\", \"$\", \"\"); },\n        '_9': /^_([+\\-]?[0-9]+|[^\\\\])/,\n        '_\\\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"_\", /^\\\\[a-zA-Z]+\\{/, \"}\", \"\", \"\", \"{\", \"}\", \"\", true); },\n        '_\\\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"_\", /^\\\\[a-zA-Z]+\\{/, \"}\", \"\"); },\n        '_\\\\x': /^_(\\\\[a-zA-Z]+)\\s*/,\n        '^_': /^(?:\\^(?=_)|\\_(?=\\^)|[\\^_]$)/,\n        '{}': /^\\{\\}/,\n        '{...}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\", \"{\", \"}\", \"\"); },\n        '{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"{\", \"\", \"\", \"}\"); },\n        '$...$': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\", \"$\", \"$\", \"\"); },\n        '${(...)}$': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"${\", \"\", \"\", \"}$\"); },\n        '$(...)$': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"$\", \"\", \"\", \"$\"); },\n        '=<>': /^[=<>]/,\n        '#': /^[#\\u2261]/,\n        '+': /^\\+/,\n        '-$': /^-(?=[\\s_},;\\]/]|$|\\([a-z]+\\))/,  // -space -, -; -] -/ -$ -state-of-aggregation\n        '-9': /^-(?=[0-9])/,\n        '- orbital overlap': /^-(?=(?:[spd]|sp)(?:$|[\\s,;\\)\\]\\}]))/,\n        '-': /^-/,\n        'pm-operator': /^(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)/,\n        'operator': /^(?:\\+|(?:[\\-=<>]|<<|>>|\\\\approx|\\$\\\\approx\\$)(?=\\s|$|-?[0-9]))/,\n        'arrowUpDown': /^(?:v|\\(v\\)|\\^|\\(\\^\\))(?=$|[\\s,;\\)\\]\\}])/,\n        '\\\\bond{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\bond{\", \"\", \"\", \"}\"); },\n        '->': /^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\\u2192\\u27F6\\u21CC])/,\n        'CMT': /^[CMT](?=\\[)/,\n        '[(...)]': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"[\", \"\", \"\", \"]\"); },\n        '1st-level escape': /^(&|\\\\\\\\|\\\\hline)\\s*/,\n        '\\\\,': /^(?:\\\\[,\\ ;:])/,  // \\\\x - but output no space before\n        '\\\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\", /^\\\\[a-zA-Z]+\\{/, \"}\", \"\", \"\", \"{\", \"}\", \"\", true); },\n        '\\\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\", /^\\\\[a-zA-Z]+\\{/, \"}\", \"\"); },\n        '\\\\ca': /^\\\\ca(?:\\s+|(?![a-zA-Z]))/,\n        '\\\\x': /^(?:\\\\[a-zA-Z]+\\s*|\\\\[_&{}%])/,\n        '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\n        'others': /^[\\/~|]/,\n        '\\\\frac{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\frac{\", \"\", \"\", \"}\", \"{\", \"\", \"\", \"}\"); },\n        '\\\\overset{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\overset{\", \"\", \"\", \"}\", \"{\", \"\", \"\", \"}\"); },\n        '\\\\underset{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\underset{\", \"\", \"\", \"}\", \"{\", \"\", \"\", \"}\"); },\n        '\\\\underbrace{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\underbrace{\", \"\", \"\", \"}_\", \"{\", \"\", \"\", \"}\"); },\n        '\\\\color{(...)}0': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\color{\", \"\", \"\", \"}\"); },\n        '\\\\color{(...)}{(...)}1': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\color{\", \"\", \"\", \"}\", \"{\", \"\", \"\", \"}\"); },\n        '\\\\color(...){(...)}2': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\color\", \"\\\\\", \"\", /^(?=\\{)/, \"{\", \"\", \"\", \"}\"); },\n        '\\\\ce{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, \"\\\\ce{\", \"\", \"\", \"}\"); },\n        'oxidation$': /^(?:[+-][IVX]+|\\\\pm\\s*0|\\$\\\\pm\\$\\s*0)$/,\n        'd-oxidation$': /^(?:[+-]?\\s?[IVX]+|\\\\pm\\s*0|\\$\\\\pm\\$\\s*0)$/,  // 0 could be oxidation or charge\n        'roman numeral': /^[IVX]+/,\n        '1/2$': /^[+\\-]?(?:[0-9]+|\\$[a-z]\\$|[a-z])\\/[0-9]+(?:\\$[a-z]\\$|[a-z])?$/,\n        'amount': function (input) {\n          var match;\n          // e.g. 2, 0.5, 1/2, -2, n/2, +;  $a$ could be added later in parsing\n          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))/);\n          if (match) {\n            return { match_: match[0], remainder: input.substr(match[0].length) };\n          }\n          var a = mhchemParser.patterns.findObserveGroups(input, \"\", \"$\", \"$\", \"\");\n          if (a) {  // e.g. $2n-1$, $-$\n            match = a.match_.match(/^\\$(?:\\(?[+\\-]?(?:[0-9]*[a-z]?[+\\-])?[0-9]*[a-z](?:[+\\-][0-9]*[a-z]?)?\\)?|\\+|-)\\$$/);\n            if (match) {\n              return { match_: match[0], remainder: input.substr(match[0].length) };\n            }\n          }\n          return null;\n        },\n        'amount2': function (input) { return this['amount'](input); },\n        '(KV letters),': /^(?:[A-Z][a-z]{0,2}|i)(?=,)/,\n        'formula$': function (input) {\n          if (input.match(/^\\([a-z]+\\)$/)) { return null; }  // state of aggregation = no formula\n          var match = input.match(/^(?:[a-z]|(?:[0-9\\ \\+\\-\\,\\.\\(\\)]+[a-z])+[0-9\\ \\+\\-\\,\\.\\(\\)]*|(?:[a-z][0-9\\ \\+\\-\\,\\.\\(\\)]+)+[a-z]?)$/);\n          if (match) {\n            return { match_: match[0], remainder: input.substr(match[0].length) };\n          }\n          return null;\n        },\n        'uprightEntities': /^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,\n        '/': /^\\s*(\\/)\\s*/,\n        '//': /^\\s*(\\/\\/)\\s*/,\n        '*': /^\\s*[*.]\\s*/\n      },\n      findObserveGroups: function (input, begExcl, begIncl, endIncl, endExcl, beg2Excl, beg2Incl, end2Incl, end2Excl, combine) {\n        /** @type {{(input: string, pattern: string | RegExp): string | string[] | null;}} */\n        var _match = function (input, pattern) {\n          if (typeof pattern === \"string\") {\n            if (input.indexOf(pattern) !== 0) { return null; }\n            return pattern;\n          } else {\n            var match = input.match(pattern);\n            if (!match) { return null; }\n            return match[0];\n          }\n        };\n        /** @type {{(input: string, i: number, endChars: string | RegExp): {endMatchBegin: number, endMatchEnd: number} | null;}} */\n        var _findObserveGroups = function (input, i, endChars) {\n          var braces = 0;\n          while (i < input.length) {\n            var a = input.charAt(i);\n            var match = _match(input.substr(i), endChars);\n            if (match !== null  &&  braces === 0) {\n              return { endMatchBegin: i, endMatchEnd: i + match.length };\n            } else if (a === \"{\") {\n              braces++;\n            } else if (a === \"}\") {\n              if (braces === 0) {\n                throw [\"ExtraCloseMissingOpen\", \"Extra close brace or missing open brace\"];\n              } else {\n                braces--;\n              }\n            }\n            i++;\n          }\n          if (braces > 0) {\n            return null;\n          }\n          return null;\n        };\n        var match = _match(input, begExcl);\n        if (match === null) { return null; }\n        input = input.substr(match.length);\n        match = _match(input, begIncl);\n        if (match === null) { return null; }\n        var e = _findObserveGroups(input, match.length, endIncl || endExcl);\n        if (e === null) { return null; }\n        var match1 = input.substring(0, (endIncl ? e.endMatchEnd : e.endMatchBegin));\n        if (!(beg2Excl || beg2Incl)) {\n          return {\n            match_: match1,\n            remainder: input.substr(e.endMatchEnd)\n          };\n        } else {\n          var group2 = this.findObserveGroups(input.substr(e.endMatchEnd), beg2Excl, beg2Incl, end2Incl, end2Excl);\n          if (group2 === null) { return null; }\n          /** @type {string[]} */\n          var matchRet = [match1, group2.match_];\n          return {\n            match_: (combine ? matchRet.join(\"\") : matchRet),\n            remainder: group2.remainder\n          };\n        }\n      },\n\n      //\n      // Matching function\n      // e.g. match(\"a\", input) will look for the regexp called \"a\" and see if it matches\n      // returns null or {match_:\"a\", remainder:\"bc\"}\n      //\n      match_: function (m, input) {\n        var pattern = mhchemParser.patterns.patterns[m];\n        if (pattern === undefined) {\n          throw [\"MhchemBugP\", \"mhchem bug P. Please report. (\" + m + \")\"];  // Trying to use non-existing pattern\n        } else if (typeof pattern === \"function\") {\n          return mhchemParser.patterns.patterns[m](input);  // cannot use cached var pattern here, because some pattern functions need this===mhchemParser\n        } else {  // RegExp\n          var match = input.match(pattern);\n          if (match) {\n            var mm;\n            if (match[2]) {\n              mm = [ match[1], match[2] ];\n            } else if (match[1]) {\n              mm = match[1];\n            } else {\n              mm = match[0];\n            }\n            return { match_: mm, remainder: input.substr(match[0].length) };\n          }\n          return null;\n        }\n      }\n    },\n\n    //\n    // Generic state machine actions\n    //\n    actions: {\n      'a=': function (buffer, m) { buffer.a = (buffer.a || \"\") + m; },\n      'b=': function (buffer, m) { buffer.b = (buffer.b || \"\") + m; },\n      'p=': function (buffer, m) { buffer.p = (buffer.p || \"\") + m; },\n      'o=': function (buffer, m) { buffer.o = (buffer.o || \"\") + m; },\n      'q=': function (buffer, m) { buffer.q = (buffer.q || \"\") + m; },\n      'd=': function (buffer, m) { buffer.d = (buffer.d || \"\") + m; },\n      'rm=': function (buffer, m) { buffer.rm = (buffer.rm || \"\") + m; },\n      'text=': function (buffer, m) { buffer.text_ = (buffer.text_ || \"\") + m; },\n      'insert': function (buffer, m, a) { return { type_: a }; },\n      'insert+p1': function (buffer, m, a) { return { type_: a, p1: m }; },\n      'insert+p1+p2': function (buffer, m, a) { return { type_: a, p1: m[0], p2: m[1] }; },\n      'copy': function (buffer, m) { return m; },\n      'rm': function (buffer, m) { return { type_: 'rm', p1: m || \"\"}; },\n      'text': function (buffer, m) { return mhchemParser.go(m, 'text'); },\n      '{text}': function (buffer, m) {\n        var ret = [ \"{\" ];\n        mhchemParser.concatArray(ret, mhchemParser.go(m, 'text'));\n        ret.push(\"}\");\n        return ret;\n      },\n      'tex-math': function (buffer, m) { return mhchemParser.go(m, 'tex-math'); },\n      'tex-math tight': function (buffer, m) { return mhchemParser.go(m, 'tex-math tight'); },\n      'bond': function (buffer, m, k) { return { type_: 'bond', kind_: k || m }; },\n      'color0-output': function (buffer, m) { return { type_: 'color0', color: m[0] }; },\n      'ce': function (buffer, m) { return mhchemParser.go(m); },\n      '1/2': function (buffer, m) {\n        /** @type {ParserOutput[]} */\n        var ret = [];\n        if (m.match(/^[+\\-]/)) {\n          ret.push(m.substr(0, 1));\n          m = m.substr(1);\n        }\n        var n = m.match(/^([0-9]+|\\$[a-z]\\$|[a-z])\\/([0-9]+)(\\$[a-z]\\$|[a-z])?$/);\n        n[1] = n[1].replace(/\\$/g, \"\");\n        ret.push({ type_: 'frac', p1: n[1], p2: n[2] });\n        if (n[3]) {\n          n[3] = n[3].replace(/\\$/g, \"\");\n          ret.push({ type_: 'tex-math', p1: n[3] });\n        }\n        return ret;\n      },\n      '9,9': function (buffer, m) { return mhchemParser.go(m, '9,9'); }\n    },\n    //\n    // createTransitions\n    // convert  { 'letter': { 'state': { action_: 'output' } } }  to  { 'state' => [ { pattern: 'letter', task: { action_: [{type_: 'output'}] } } ] }\n    // with expansion of 'a|b' to 'a' and 'b' (at 2 places)\n    //\n    createTransitions: function (o) {\n      var pattern, state;\n      /** @type {string[]} */\n      var stateArray;\n      var i;\n      //\n      // 1. Collect all states\n      //\n      /** @type {Transitions} */\n      var transitions = {};\n      for (pattern in o) {\n        for (state in o[pattern]) {\n          stateArray = state.split(\"|\");\n          o[pattern][state].stateArray = stateArray;\n          for (i=0; i<stateArray.length; i++) {\n            transitions[stateArray[i]] = [];\n          }\n        }\n      }\n      //\n      // 2. Fill states\n      //\n      for (pattern in o) {\n        for (state in o[pattern]) {\n          stateArray = o[pattern][state].stateArray || [];\n          for (i=0; i<stateArray.length; i++) {\n            //\n            // 2a. Normalize actions into array:  'text=' ==> [{type_:'text='}]\n            // (Note to myself: Resolving the function here would be problematic. It would need .bind (for *this*) and currying (for *option*).)\n            //\n            /** @type {any} */\n            var p = o[pattern][state];\n            if (p.action_) {\n              p.action_ = [].concat(p.action_);\n              for (var k=0; k<p.action_.length; k++) {\n                if (typeof p.action_[k] === \"string\") {\n                  p.action_[k] = { type_: p.action_[k] };\n                }\n              }\n            } else {\n              p.action_ = [];\n            }\n            //\n            // 2.b Multi-insert\n            //\n            var patternArray = pattern.split(\"|\");\n            for (var j=0; j<patternArray.length; j++) {\n              if (stateArray[i] === '*') {  // insert into all\n                for (var t in transitions) {\n                  transitions[t].push({ pattern: patternArray[j], task: p });\n                }\n              } else {\n                transitions[stateArray[i]].push({ pattern: patternArray[j], task: p });\n              }\n            }\n          }\n        }\n      }\n      return transitions;\n    },\n    stateMachines: {}\n  };\n\n  //\n  // Definition of state machines\n  //\n  mhchemParser.stateMachines = {\n    //\n    // \\ce state machines\n    //\n    //#region ce\n    'ce': {  // main parser\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': { action_: 'output' } },\n        'else':  {\n          '0|1|2': { action_: 'beginsWithBond=false', revisit: true, toContinue: true } },\n        'oxidation$': {\n          '0': { action_: 'oxidation-output' } },\n        'CMT': {\n          'r': { action_: 'rdt=', nextState: 'rt' },\n          'rd': { action_: 'rqt=', nextState: 'rdt' } },\n        'arrowUpDown': {\n          '0|1|2|as': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '1' } },\n        'uprightEntities': {\n          '0|1|2': { action_: [ 'o=', 'output' ], nextState: '1' } },\n        'orbital': {\n          '0|1|2|3': { action_: 'o=', nextState: 'o' } },\n        '->': {\n          '0|1|2|3': { action_: 'r=', nextState: 'r' },\n          'a|as': { action_: [ 'output', 'r=' ], nextState: 'r' },\n          '*': { action_: [ 'output', 'r=' ], nextState: 'r' } },\n        '+': {\n          'o': { action_: 'd= kv',  nextState: 'd' },\n          'd|D': { action_: 'd=', nextState: 'd' },\n          'q': { action_: 'd=',  nextState: 'qd' },\n          'qd|qD': { action_: 'd=', nextState: 'qd' },\n          'dq': { action_: [ 'output', 'd=' ], nextState: 'd' },\n          '3': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '0' } },\n        'amount': {\n          '0|2': { action_: 'a=', nextState: 'a' } },\n        'pm-operator': {\n          '0|1|2|a|as': { action_: [ 'sb=false', 'output', { type_: 'operator', option: '\\\\pm' } ], nextState: '0' } },\n        'operator': {\n          '0|1|2|a|as': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '0' } },\n        '-$': {\n          'o|q': { action_: [ 'charge or bond', 'output' ],  nextState: 'qd' },\n          'd': { action_: 'd=', nextState: 'd' },\n          'D': { action_: [ 'output', { type_: 'bond', option: \"-\" } ], nextState: '3' },\n          'q': { action_: 'd=',  nextState: 'qd' },\n          'qd': { action_: 'd=', nextState: 'qd' },\n          'qD|dq': { action_: [ 'output', { type_: 'bond', option: \"-\" } ], nextState: '3' } },\n        '-9': {\n          '3|o': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '3' } },\n        '- orbital overlap': {\n          'o': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' },\n          'd': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' } },\n        '-': {\n          '0|1|2': { action_: [ { type_: 'output', option: 1 }, 'beginsWithBond=true', { type_: 'bond', option: \"-\" } ], nextState: '3' },\n          '3': { action_: { type_: 'bond', option: \"-\" } },\n          'a': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' },\n          'as': { action_: [ { type_: 'output', option: 2 }, { type_: 'bond', option: \"-\" } ], nextState: '3' },\n          'b': { action_: 'b=' },\n          'o': { action_: { type_: '- after o/d', option: false }, nextState: '2' },\n          'q': { action_: { type_: '- after o/d', option: false }, nextState: '2' },\n          'd|qd|dq': { action_: { type_: '- after o/d', option: true }, nextState: '2' },\n          'D|qD|p': { action_: [ 'output', { type_: 'bond', option: \"-\" } ], nextState: '3' } },\n        'amount2': {\n          '1|3': { action_: 'a=', nextState: 'a' } },\n        'letters': {\n          '0|1|2|3|a|as|b|p|bp|o': { action_: 'o=', nextState: 'o' },\n          'q|dq': { action_: ['output', 'o='], nextState: 'o' },\n          'd|D|qd|qD': { action_: 'o after d', nextState: 'o' } },\n        'digits': {\n          'o': { action_: 'q=', nextState: 'q' },\n          'd|D': { action_: 'q=', nextState: 'dq' },\n          'q': { action_: [ 'output', 'o=' ], nextState: 'o' },\n          'a': { action_: 'o=', nextState: 'o' } },\n        'space A': {\n          'b|p|bp': {} },\n        'space': {\n          'a': { nextState: 'as' },\n          '0': { action_: 'sb=false' },\n          '1|2': { action_: 'sb=true' },\n          'r|rt|rd|rdt|rdq': { action_: 'output', nextState: '0' },\n          '*': { action_: [ 'output', 'sb=true' ], nextState: '1'} },\n        '1st-level escape': {\n          '1|2': { action_: [ 'output', { type_: 'insert+p1', option: '1st-level escape' } ] },\n          '*': { action_: [ 'output', { type_: 'insert+p1', option: '1st-level escape' } ], nextState: '0' } },\n        '[(...)]': {\n          'r|rt': { action_: 'rd=', nextState: 'rd' },\n          'rd|rdt': { action_: 'rq=', nextState: 'rdq' } },\n        '...': {\n          'o|d|D|dq|qd|qD': { action_: [ 'output', { type_: 'bond', option: \"...\" } ], nextState: '3' },\n          '*': { action_: [ { type_: 'output', option: 1 }, { type_: 'insert', option: 'ellipsis' } ], nextState: '1' } },\n        '. |* ': {\n          '*': { action_: [ 'output', { type_: 'insert', option: 'addition compound' } ], nextState: '1' } },\n        'state of aggregation $': {\n          '*': { action_: [ 'output', 'state of aggregation' ], nextState: '1' } },\n        '{[(': {\n          'a|as|o': { action_: [ 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' },\n          '0|1|2|3': { action_: [ 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' },\n          '*': { action_: [ 'output', 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' } },\n        ')]}': {\n          '0|1|2|3|b|p|bp|o': { action_: [ 'o=', 'parenthesisLevel--' ], nextState: 'o' },\n          'a|as|d|D|q|qd|qD|dq': { action_: [ 'output', 'o=', 'parenthesisLevel--' ], nextState: 'o' } },\n        ', ': {\n          '*': { action_: [ 'output', 'comma' ], nextState: '0' } },\n        '^_': {  // ^ and _ without a sensible argument\n          '*': { } },\n        '^{(...)}|^($...$)': {\n          '0|1|2|as': { action_: 'b=', nextState: 'b' },\n          'p': { action_: 'b=', nextState: 'bp' },\n          '3|o': { action_: 'd= kv', nextState: 'D' },\n          'q': { action_: 'd=', nextState: 'qD' },\n          'd|D|qd|qD|dq': { action_: [ 'output', 'd=' ], nextState: 'D' } },\n        '^a|^\\\\x{}{}|^\\\\x{}|^\\\\x|\\'': {\n          '0|1|2|as': { action_: 'b=', nextState: 'b' },\n          'p': { action_: 'b=', nextState: 'bp' },\n          '3|o': { action_: 'd= kv', nextState: 'd' },\n          'q': { action_: 'd=', nextState: 'qd' },\n          'd|qd|D|qD': { action_: 'd=' },\n          'dq': { action_: [ 'output', 'd=' ], nextState: 'd' } },\n        '_{(state of aggregation)}$': {\n          'd|D|q|qd|qD|dq': { action_: [ 'output', 'q=' ], nextState: 'q' } },\n        '_{(...)}|_($...$)|_9|_\\\\x{}{}|_\\\\x{}|_\\\\x': {\n          '0|1|2|as': { action_: 'p=', nextState: 'p' },\n          'b': { action_: 'p=', nextState: 'bp' },\n          '3|o': { action_: 'q=', nextState: 'q' },\n          'd|D': { action_: 'q=', nextState: 'dq' },\n          'q|qd|qD|dq': { action_: [ 'output', 'q=' ], nextState: 'q' } },\n        '=<>': {\n          '0|1|2|3|a|as|o|q|d|D|qd|qD|dq': { action_: [ { type_: 'output', option: 2 }, 'bond' ], nextState: '3' } },\n        '#': {\n          '0|1|2|3|a|as|o': { action_: [ { type_: 'output', option: 2 }, { type_: 'bond', option: \"#\" } ], nextState: '3' } },\n        '{}': {\n          '*': { action_: { type_: 'output', option: 1 },  nextState: '1' } },\n        '{...}': {\n          '0|1|2|3|a|as|b|p|bp': { action_: 'o=', nextState: 'o' },\n          'o|d|D|q|qd|qD|dq': { action_: [ 'output', 'o=' ], nextState: 'o' } },\n        '$...$': {\n          'a': { action_: 'a=' },  // 2$n$\n          '0|1|2|3|as|b|p|bp|o': { action_: 'o=', nextState: 'o' },  // not 'amount'\n          'as|o': { action_: 'o=' },\n          'q|d|D|qd|qD|dq': { action_: [ 'output', 'o=' ], nextState: 'o' } },\n        '\\\\bond{(...)}': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'bond' ], nextState: \"3\" } },\n        '\\\\frac{(...)}': {\n          '*': { action_: [ { type_: 'output', option: 1 }, 'frac-output' ], nextState: '3' } },\n        '\\\\overset{(...)}': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'overset-output' ], nextState: '3' } },\n        '\\\\underset{(...)}': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'underset-output' ], nextState: '3' } },\n        '\\\\underbrace{(...)}': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'underbrace-output' ], nextState: '3' } },\n        '\\\\color{(...)}{(...)}1|\\\\color(...){(...)}2': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'color-output' ], nextState: '3' } },\n        '\\\\color{(...)}0': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'color0-output' ] } },\n        '\\\\ce{(...)}': {\n          '*': { action_: [ { type_: 'output', option: 2 }, 'ce' ], nextState: '3' } },\n        '\\\\,': {\n          '*': { action_: [ { type_: 'output', option: 1 }, 'copy' ], nextState: '1' } },\n        '\\\\x{}{}|\\\\x{}|\\\\x': {\n          '0|1|2|3|a|as|b|p|bp|o|c0': { action_: [ 'o=', 'output' ], nextState: '3' },\n          '*': { action_: ['output', 'o=', 'output' ], nextState: '3' } },\n        'others': {\n          '*': { action_: [ { type_: 'output', option: 1 }, 'copy' ], nextState: '3' } },\n        'else2': {\n          'a': { action_: 'a to o', nextState: 'o', revisit: true },\n          'as': { action_: [ 'output', 'sb=true' ], nextState: '1', revisit: true },\n          'r|rt|rd|rdt|rdq': { action_: [ 'output' ], nextState: '0', revisit: true },\n          '*': { action_: [ 'output', 'copy' ], nextState: '3' } }\n      }),\n      actions: {\n        'o after d': function (buffer, m) {\n          var ret;\n          if ((buffer.d || \"\").match(/^[0-9]+$/)) {\n            var tmp = buffer.d;\n            buffer.d = undefined;\n            ret = this['output'](buffer);\n            buffer.b = tmp;\n          } else {\n            ret = this['output'](buffer);\n          }\n          mhchemParser.actions['o='](buffer, m);\n          return ret;\n        },\n        'd= kv': function (buffer, m) {\n          buffer.d = m;\n          buffer.dType = 'kv';\n        },\n        'charge or bond': function (buffer, m) {\n          if (buffer['beginsWithBond']) {\n            /** @type {ParserOutput[]} */\n            var ret = [];\n            mhchemParser.concatArray(ret, this['output'](buffer));\n            mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, \"-\"));\n            return ret;\n          } else {\n            buffer.d = m;\n          }\n        },\n        '- after o/d': function (buffer, m, isAfterD) {\n          var c1 = mhchemParser.patterns.match_('orbital', buffer.o || \"\");\n          var c2 = mhchemParser.patterns.match_('one lowercase greek letter $', buffer.o || \"\");\n          var c3 = mhchemParser.patterns.match_('one lowercase latin letter $', buffer.o || \"\");\n          var c4 = mhchemParser.patterns.match_('$one lowercase latin letter$ $', buffer.o || \"\");\n          var hyphenFollows =  m===\"-\" && ( c1 && c1.remainder===\"\"  ||  c2  ||  c3  ||  c4 );\n          if (hyphenFollows && !buffer.a && !buffer.b && !buffer.p && !buffer.d && !buffer.q && !c1 && c3) {\n            buffer.o = '$' + buffer.o + '$';\n          }\n          /** @type {ParserOutput[]} */\n          var ret = [];\n          if (hyphenFollows) {\n            mhchemParser.concatArray(ret, this['output'](buffer));\n            ret.push({ type_: 'hyphen' });\n          } else {\n            c1 = mhchemParser.patterns.match_('digits', buffer.d || \"\");\n            if (isAfterD && c1 && c1.remainder==='') {\n              mhchemParser.concatArray(ret, mhchemParser.actions['d='](buffer, m));\n              mhchemParser.concatArray(ret, this['output'](buffer));\n            } else {\n              mhchemParser.concatArray(ret, this['output'](buffer));\n              mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, \"-\"));\n            }\n          }\n          return ret;\n        },\n        'a to o': function (buffer) {\n          buffer.o = buffer.a;\n          buffer.a = undefined;\n        },\n        'sb=true': function (buffer) { buffer.sb = true; },\n        'sb=false': function (buffer) { buffer.sb = false; },\n        'beginsWithBond=true': function (buffer) { buffer['beginsWithBond'] = true; },\n        'beginsWithBond=false': function (buffer) { buffer['beginsWithBond'] = false; },\n        'parenthesisLevel++': function (buffer) { buffer['parenthesisLevel']++; },\n        'parenthesisLevel--': function (buffer) { buffer['parenthesisLevel']--; },\n        'state of aggregation': function (buffer, m) {\n          return { type_: 'state of aggregation', p1: mhchemParser.go(m, 'o') };\n        },\n        'comma': function (buffer, m) {\n          var a = m.replace(/\\s*$/, '');\n          var withSpace = (a !== m);\n          if (withSpace  &&  buffer['parenthesisLevel'] === 0) {\n            return { type_: 'comma enumeration L', p1: a };\n          } else {\n            return { type_: 'comma enumeration M', p1: a };\n          }\n        },\n        'output': function (buffer, m, entityFollows) {\n          // entityFollows:\n          //   undefined = if we have nothing else to output, also ignore the just read space (buffer.sb)\n          //   1 = an entity follows, never omit the space if there was one just read before (can only apply to state 1)\n          //   2 = 1 + the entity can have an amount, so output a\\, instead of converting it to o (can only apply to states a|as)\n          /** @type {ParserOutput | ParserOutput[]} */\n          var ret;\n          if (!buffer.r) {\n            ret = [];\n            if (!buffer.a && !buffer.b && !buffer.p && !buffer.o && !buffer.q && !buffer.d && !entityFollows) {\n              //ret = [];\n            } else {\n              if (buffer.sb) {\n                ret.push({ type_: 'entitySkip' });\n              }\n              if (!buffer.o && !buffer.q && !buffer.d && !buffer.b && !buffer.p && entityFollows!==2) {\n                buffer.o = buffer.a;\n                buffer.a = undefined;\n              } else if (!buffer.o && !buffer.q && !buffer.d && (buffer.b || buffer.p)) {\n                buffer.o = buffer.a;\n                buffer.d = buffer.b;\n                buffer.q = buffer.p;\n                buffer.a = buffer.b = buffer.p = undefined;\n              } else {\n                if (buffer.o && buffer.dType==='kv' && mhchemParser.patterns.match_('d-oxidation$', buffer.d || \"\")) {\n                  buffer.dType = 'oxidation';\n                } else if (buffer.o && buffer.dType==='kv' && !buffer.q) {\n                  buffer.dType = undefined;\n                }\n              }\n              ret.push({\n                type_: 'chemfive',\n                a: mhchemParser.go(buffer.a, 'a'),\n                b: mhchemParser.go(buffer.b, 'bd'),\n                p: mhchemParser.go(buffer.p, 'pq'),\n                o: mhchemParser.go(buffer.o, 'o'),\n                q: mhchemParser.go(buffer.q, 'pq'),\n                d: mhchemParser.go(buffer.d, (buffer.dType === 'oxidation' ? 'oxidation' : 'bd')),\n                dType: buffer.dType\n              });\n            }\n          } else {  // r\n            /** @type {ParserOutput[]} */\n            var rd;\n            if (buffer.rdt === 'M') {\n              rd = mhchemParser.go(buffer.rd, 'tex-math');\n            } else if (buffer.rdt === 'T') {\n              rd = [ { type_: 'text', p1: buffer.rd || \"\" } ];\n            } else {\n              rd = mhchemParser.go(buffer.rd);\n            }\n            /** @type {ParserOutput[]} */\n            var rq;\n            if (buffer.rqt === 'M') {\n              rq = mhchemParser.go(buffer.rq, 'tex-math');\n            } else if (buffer.rqt === 'T') {\n              rq = [ { type_: 'text', p1: buffer.rq || \"\"} ];\n            } else {\n              rq = mhchemParser.go(buffer.rq);\n            }\n            ret = {\n              type_: 'arrow',\n              r: buffer.r,\n              rd: rd,\n              rq: rq\n            };\n          }\n          for (var p in buffer) {\n            if (p !== 'parenthesisLevel'  &&  p !== 'beginsWithBond') {\n              delete buffer[p];\n            }\n          }\n          return ret;\n        },\n        'oxidation-output': function (buffer, m) {\n          var ret = [ \"{\" ];\n          mhchemParser.concatArray(ret, mhchemParser.go(m, 'oxidation'));\n          ret.push(\"}\");\n          return ret;\n        },\n        'frac-output': function (buffer, m) {\n          return { type_: 'frac-ce', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };\n        },\n        'overset-output': function (buffer, m) {\n          return { type_: 'overset', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };\n        },\n        'underset-output': function (buffer, m) {\n          return { type_: 'underset', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };\n        },\n        'underbrace-output': function (buffer, m) {\n          return { type_: 'underbrace', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };\n        },\n        'color-output': function (buffer, m) {\n          return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1]) };\n        },\n        'r=': function (buffer, m) { buffer.r = m; },\n        'rdt=': function (buffer, m) { buffer.rdt = m; },\n        'rd=': function (buffer, m) { buffer.rd = m; },\n        'rqt=': function (buffer, m) { buffer.rqt = m; },\n        'rq=': function (buffer, m) { buffer.rq = m; },\n        'operator': function (buffer, m, p1) { return { type_: 'operator', kind_: (p1 || m) }; }\n      }\n    },\n    'a': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': {} },\n        '1/2$': {\n          '0': { action_: '1/2' } },\n        'else': {\n          '0': { nextState: '1', revisit: true } },\n        '$(...)$': {\n          '*': { action_: 'tex-math tight', nextState: '1' } },\n        ',': {\n          '*': { action_: { type_: 'insert', option: 'commaDecimal' } } },\n        'else2': {\n          '*': { action_: 'copy' } }\n      }),\n      actions: {}\n    },\n    'o': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': {} },\n        '1/2$': {\n          '0': { action_: '1/2' } },\n        'else': {\n          '0': { nextState: '1', revisit: true } },\n        'letters': {\n          '*': { action_: 'rm' } },\n        '\\\\ca': {\n          '*': { action_: { type_: 'insert', option: 'circa' } } },\n        '\\\\x{}{}|\\\\x{}|\\\\x': {\n          '*': { action_: 'copy' } },\n        '${(...)}$|$(...)$': {\n          '*': { action_: 'tex-math' } },\n        '{(...)}': {\n          '*': { action_: '{text}' } },\n        'else2': {\n          '*': { action_: 'copy' } }\n      }),\n      actions: {}\n    },\n    'text': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': { action_: 'output' } },\n        '{...}': {\n          '*': { action_: 'text=' } },\n        '${(...)}$|$(...)$': {\n          '*': { action_: 'tex-math' } },\n        '\\\\greek': {\n          '*': { action_: [ 'output', 'rm' ] } },\n        '\\\\,|\\\\x{}{}|\\\\x{}|\\\\x': {\n          '*': { action_: [ 'output', 'copy' ] } },\n        'else': {\n          '*': { action_: 'text=' } }\n      }),\n      actions: {\n        'output': function (buffer) {\n          if (buffer.text_) {\n            /** @type {ParserOutput} */\n            var ret = { type_: 'text', p1: buffer.text_ };\n            for (var p in buffer) { delete buffer[p]; }\n            return ret;\n          }\n        }\n      }\n    },\n    'pq': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': {} },\n        'state of aggregation $': {\n          '*': { action_: 'state of aggregation' } },\n        'i$': {\n          '0': { nextState: '!f', revisit: true } },\n        '(KV letters),': {\n          '0': { action_: 'rm', nextState: '0' } },\n        'formula$': {\n          '0': { nextState: 'f', revisit: true } },\n        '1/2$': {\n          '0': { action_: '1/2' } },\n        'else': {\n          '0': { nextState: '!f', revisit: true } },\n        '${(...)}$|$(...)$': {\n          '*': { action_: 'tex-math' } },\n        '{(...)}': {\n          '*': { action_: 'text' } },\n        'a-z': {\n          'f': { action_: 'tex-math' } },\n        'letters': {\n          '*': { action_: 'rm' } },\n        '-9.,9': {\n          '*': { action_: '9,9'  } },\n        ',': {\n          '*': { action_: { type_: 'insert+p1', option: 'comma enumeration S' } } },\n        '\\\\color{(...)}{(...)}1|\\\\color(...){(...)}2': {\n          '*': { action_: 'color-output' } },\n        '\\\\color{(...)}0': {\n          '*': { action_: 'color0-output' } },\n        '\\\\ce{(...)}': {\n          '*': { action_: 'ce' } },\n        '\\\\,|\\\\x{}{}|\\\\x{}|\\\\x': {\n          '*': { action_: 'copy' } },\n        'else2': {\n          '*': { action_: 'copy' } }\n      }),\n      actions: {\n        'state of aggregation': function (buffer, m) {\n          return { type_: 'state of aggregation subscript', p1: mhchemParser.go(m, 'o') };\n        },\n        'color-output': function (buffer, m) {\n          return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1], 'pq') };\n        }\n      }\n    },\n    'bd': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': {} },\n        'x$': {\n          '0': { nextState: '!f', revisit: true } },\n        'formula$': {\n          '0': { nextState: 'f', revisit: true } },\n        'else': {\n          '0': { nextState: '!f', revisit: true } },\n        '-9.,9 no missing 0': {\n          '*': { action_: '9,9' } },\n        '.': {\n          '*': { action_: { type_: 'insert', option: 'electron dot' } } },\n        'a-z': {\n          'f': { action_: 'tex-math' } },\n        'x': {\n          '*': { action_: { type_: 'insert', option: 'KV x' } } },\n        'letters': {\n          '*': { action_: 'rm' } },\n        '\\'': {\n          '*': { action_: { type_: 'insert', option: 'prime' } } },\n        '${(...)}$|$(...)$': {\n          '*': { action_: 'tex-math' } },\n        '{(...)}': {\n          '*': { action_: 'text' } },\n        '\\\\color{(...)}{(...)}1|\\\\color(...){(...)}2': {\n          '*': { action_: 'color-output' } },\n        '\\\\color{(...)}0': {\n          '*': { action_: 'color0-output' } },\n        '\\\\ce{(...)}': {\n          '*': { action_: 'ce' } },\n        '\\\\,|\\\\x{}{}|\\\\x{}|\\\\x': {\n          '*': { action_: 'copy' } },\n        'else2': {\n          '*': { action_: 'copy' } }\n      }),\n      actions: {\n        'color-output': function (buffer, m) {\n          return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1], 'bd') };\n        }\n      }\n    },\n    'oxidation': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': {} },\n        'roman numeral': {\n          '*': { action_: 'roman-numeral' } },\n        '${(...)}$|$(...)$': {\n          '*': { action_: 'tex-math' } },\n        'else': {\n          '*': { action_: 'copy' } }\n      }),\n      actions: {\n        'roman-numeral': function (buffer, m) { return { type_: 'roman numeral', p1: m || \"\" }; }\n      }\n    },\n    'tex-math': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': { action_: 'output' } },\n        '\\\\ce{(...)}': {\n          '*': { action_: [ 'output', 'ce' ] } },\n        '{...}|\\\\,|\\\\x{}{}|\\\\x{}|\\\\x': {\n          '*': { action_: 'o=' } },\n        'else': {\n          '*': { action_: 'o=' } }\n      }),\n      actions: {\n        'output': function (buffer) {\n          if (buffer.o) {\n            /** @type {ParserOutput} */\n            var ret = { type_: 'tex-math', p1: buffer.o };\n            for (var p in buffer) { delete buffer[p]; }\n            return ret;\n          }\n        }\n      }\n    },\n    'tex-math tight': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': { action_: 'output' } },\n        '\\\\ce{(...)}': {\n          '*': { action_: [ 'output', 'ce' ] } },\n        '{...}|\\\\,|\\\\x{}{}|\\\\x{}|\\\\x': {\n          '*': { action_: 'o=' } },\n        '-|+': {\n          '*': { action_: 'tight operator' } },\n        'else': {\n          '*': { action_: 'o=' } }\n      }),\n      actions: {\n        'tight operator': function (buffer, m) { buffer.o = (buffer.o || \"\") + \"{\"+m+\"}\"; },\n        'output': function (buffer) {\n          if (buffer.o) {\n            /** @type {ParserOutput} */\n            var ret = { type_: 'tex-math', p1: buffer.o };\n            for (var p in buffer) { delete buffer[p]; }\n            return ret;\n          }\n        }\n      }\n    },\n    '9,9': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': {} },\n        ',': {\n          '*': { action_: 'comma' } },\n        'else': {\n          '*': { action_: 'copy' } }\n      }),\n      actions: {\n        'comma': function () { return { type_: 'commaDecimal' }; }\n      }\n    },\n    //#endregion\n    //\n    // \\pu state machines\n    //\n    //#region pu\n    'pu': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': { action_: 'output' } },\n        'space$': {\n          '*': { action_: [ 'output', 'space' ] } },\n        '{[(|)]}': {\n          '0|a': { action_: 'copy' } },\n        '(-)(9)^(-9)': {\n          '0': { action_: 'number^', nextState: 'a' } },\n        '(-)(9.,9)(e)(99)': {\n          '0': { action_: 'enumber', nextState: 'a' } },\n        'space': {\n          '0|a': {} },\n        'pm-operator': {\n          '0|a': { action_: { type_: 'operator', option: '\\\\pm' }, nextState: '0' } },\n        'operator': {\n          '0|a': { action_: 'copy', nextState: '0' } },\n        '//': {\n          'd': { action_: 'o=', nextState: '/' } },\n        '/': {\n          'd': { action_: 'o=', nextState: '/' } },\n        '{...}|else': {\n          '0|d': { action_: 'd=', nextState: 'd' },\n          'a': { action_: [ 'space', 'd=' ], nextState: 'd' },\n          '/|q': { action_: 'q=', nextState: 'q' } }\n      }),\n      actions: {\n        'enumber': function (buffer, m) {\n          /** @type {ParserOutput[]} */\n          var ret = [];\n          if (m[0] === \"+-\"  ||  m[0] === \"+/-\") {\n            ret.push(\"\\\\pm \");\n          } else if (m[0]) {\n            ret.push(m[0]);\n          }\n          if (m[1]) {\n            mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9'));\n            if (m[2]) {\n              if (m[2].match(/[,.]/)) {\n                mhchemParser.concatArray(ret, mhchemParser.go(m[2], 'pu-9,9'));\n              } else {\n                ret.push(m[2]);\n              }\n            }\n            m[3] = m[4] || m[3];\n            if (m[3]) {\n              m[3] = m[3].trim();\n              if (m[3] === \"e\"  ||  m[3].substr(0, 1) === \"*\") {\n                ret.push({ type_: 'cdot' });\n              } else {\n                ret.push({ type_: 'times' });\n              }\n            }\n          }\n          if (m[3]) {\n            ret.push(\"10^{\"+m[5]+\"}\");\n          }\n          return ret;\n        },\n        'number^': function (buffer, m) {\n          /** @type {ParserOutput[]} */\n          var ret = [];\n          if (m[0] === \"+-\"  ||  m[0] === \"+/-\") {\n            ret.push(\"\\\\pm \");\n          } else if (m[0]) {\n            ret.push(m[0]);\n          }\n          mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9'));\n          ret.push(\"^{\"+m[2]+\"}\");\n          return ret;\n        },\n        'operator': function (buffer, m, p1) { return { type_: 'operator', kind_: (p1 || m) }; },\n        'space': function () { return { type_: 'pu-space-1' }; },\n        'output': function (buffer) {\n          /** @type {ParserOutput | ParserOutput[]} */\n          var ret;\n          var md = mhchemParser.patterns.match_('{(...)}', buffer.d || \"\");\n          if (md  &&  md.remainder === '') { buffer.d = md.match_; }\n          var mq = mhchemParser.patterns.match_('{(...)}', buffer.q || \"\");\n          if (mq  &&  mq.remainder === '') { buffer.q = mq.match_; }\n          if (buffer.d) {\n            buffer.d = buffer.d.replace(/\\u00B0C|\\^oC|\\^{o}C/g, \"{}^{\\\\circ}C\");\n            buffer.d = buffer.d.replace(/\\u00B0F|\\^oF|\\^{o}F/g, \"{}^{\\\\circ}F\");\n          }\n          if (buffer.q) {  // fraction\n            buffer.q = buffer.q.replace(/\\u00B0C|\\^oC|\\^{o}C/g, \"{}^{\\\\circ}C\");\n            buffer.q = buffer.q.replace(/\\u00B0F|\\^oF|\\^{o}F/g, \"{}^{\\\\circ}F\");\n            var b5 = {\n              d: mhchemParser.go(buffer.d, 'pu'),\n              q: mhchemParser.go(buffer.q, 'pu')\n            };\n            if (buffer.o === '//') {\n              ret = { type_: 'pu-frac', p1: b5.d, p2: b5.q };\n            } else {\n              ret = b5.d;\n              if (b5.d.length > 1  ||  b5.q.length > 1) {\n                ret.push({ type_: ' / ' });\n              } else {\n                ret.push({ type_: '/' });\n              }\n              mhchemParser.concatArray(ret, b5.q);\n            }\n          } else {  // no fraction\n            ret = mhchemParser.go(buffer.d, 'pu-2');\n          }\n          for (var p in buffer) { delete buffer[p]; }\n          return ret;\n        }\n      }\n    },\n    'pu-2': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '*': { action_: 'output' } },\n        '*': {\n          '*': { action_: [ 'output', 'cdot' ], nextState: '0' } },\n        '\\\\x': {\n          '*': { action_: 'rm=' } },\n        'space': {\n          '*': { action_: [ 'output', 'space' ], nextState: '0' } },\n        '^{(...)}|^(-1)': {\n          '1': { action_: '^(-1)' } },\n        '-9.,9': {\n          '0': { action_: 'rm=', nextState: '0' },\n          '1': { action_: '^(-1)', nextState: '0' } },\n        '{...}|else': {\n          '*': { action_: 'rm=', nextState: '1' } }\n      }),\n      actions: {\n        'cdot': function () { return { type_: 'tight cdot' }; },\n        '^(-1)': function (buffer, m) { buffer.rm += \"^{\"+m+\"}\"; },\n        'space': function () { return { type_: 'pu-space-2' }; },\n        'output': function (buffer) {\n          /** @type {ParserOutput | ParserOutput[]} */\n          var ret = [];\n          if (buffer.rm) {\n            var mrm = mhchemParser.patterns.match_('{(...)}', buffer.rm || \"\");\n            if (mrm  &&  mrm.remainder === '') {\n              ret = mhchemParser.go(mrm.match_, 'pu');\n            } else {\n              ret = { type_: 'rm', p1: buffer.rm };\n            }\n          }\n          for (var p in buffer) { delete buffer[p]; }\n          return ret;\n        }\n      }\n    },\n    'pu-9,9': {\n      transitions: mhchemParser.createTransitions({\n        'empty': {\n          '0': { action_: 'output-0' },\n          'o': { action_: 'output-o' } },\n        ',': {\n          '0': { action_: [ 'output-0', 'comma' ], nextState: 'o' } },\n        '.': {\n          '0': { action_: [ 'output-0', 'copy' ], nextState: 'o' } },\n        'else': {\n          '*': { action_: 'text=' } }\n      }),\n      actions: {\n        'comma': function () { return { type_: 'commaDecimal' }; },\n        'output-0': function (buffer) {\n          /** @type {ParserOutput[]} */\n          var ret = [];\n          buffer.text_ = buffer.text_ || \"\";\n          if (buffer.text_.length > 4) {\n            var a = buffer.text_.length % 3;\n            if (a === 0) { a = 3; }\n            for (var i=buffer.text_.length-3; i>0; i-=3) {\n              ret.push(buffer.text_.substr(i, 3));\n              ret.push({ type_: '1000 separator' });\n            }\n            ret.push(buffer.text_.substr(0, a));\n            ret.reverse();\n          } else {\n            ret.push(buffer.text_);\n          }\n          for (var p in buffer) { delete buffer[p]; }\n          return ret;\n        },\n        'output-o': function (buffer) {\n          /** @type {ParserOutput[]} */\n          var ret = [];\n          buffer.text_ = buffer.text_ || \"\";\n          if (buffer.text_.length > 4) {\n            var a = buffer.text_.length - 3;\n            for (var i=0; i<a; i+=3) {\n              ret.push(buffer.text_.substr(i, 3));\n              ret.push({ type_: '1000 separator' });\n            }\n            ret.push(buffer.text_.substr(i));\n          } else {\n            ret.push(buffer.text_);\n          }\n          for (var p in buffer) { delete buffer[p]; }\n          return ret;\n        }\n      }\n    }\n    //#endregion\n  };\n\n  //\n  // texify: Take MhchemParser output and convert it to TeX\n  //\n  /** @type {Texify} */\n  var texify = {\n    go: function (input, isInner) {  // (recursive, max 4 levels)\n      if (!input) { return \"\"; }\n      var res = \"\";\n      var cee = false;\n      for (var i=0; i < input.length; i++) {\n        var inputi = input[i];\n        if (typeof inputi === \"string\") {\n          res += inputi;\n        } else {\n          res += texify._go2(inputi);\n          if (inputi.type_ === '1st-level escape') { cee = true; }\n        }\n      }\n      if (!isInner && !cee && res) {\n        res = \"{\" + res + \"}\";\n      }\n      return res;\n    },\n    _goInner: function (input) {\n      if (!input) { return input; }\n      return texify.go(input, true);\n    },\n    _go2: function (buf) {\n      /** @type {undefined | string} */\n      var res;\n      switch (buf.type_) {\n        case 'chemfive':\n          res = \"\";\n          var b5 = {\n            a: texify._goInner(buf.a),\n            b: texify._goInner(buf.b),\n            p: texify._goInner(buf.p),\n            o: texify._goInner(buf.o),\n            q: texify._goInner(buf.q),\n            d: texify._goInner(buf.d)\n          };\n          //\n          // a\n          //\n          if (b5.a) {\n            if (b5.a.match(/^[+\\-]/)) { b5.a = \"{\"+b5.a+\"}\"; }\n            res += b5.a + \"\\\\,\";\n          }\n          //\n          // b and p\n          //\n          if (b5.b || b5.p) {\n            res += \"{\\\\vphantom{X}}\";\n            res += \"^{\\\\hphantom{\"+(b5.b||\"\")+\"}}_{\\\\hphantom{\"+(b5.p||\"\")+\"}}\";\n            res += \"{\\\\vphantom{X}}\";\n            res += \"^{\\\\smash[t]{\\\\vphantom{2}}\\\\mathllap{\"+(b5.b||\"\")+\"}}\";\n            res += \"_{\\\\vphantom{2}\\\\mathllap{\\\\smash[t]{\"+(b5.p||\"\")+\"}}}\";\n          }\n          //\n          // o\n          //\n          if (b5.o) {\n            if (b5.o.match(/^[+\\-]/)) { b5.o = \"{\"+b5.o+\"}\"; }\n            res += b5.o;\n          }\n          //\n          // q and d\n          //\n          if (buf.dType === 'kv') {\n            if (b5.d || b5.q) {\n              res += \"{\\\\vphantom{X}}\";\n            }\n            if (b5.d) {\n              res += \"^{\"+b5.d+\"}\";\n            }\n            if (b5.q) {\n              res += \"_{\\\\smash[t]{\"+b5.q+\"}}\";\n            }\n          } else if (buf.dType === 'oxidation') {\n            if (b5.d) {\n              res += \"{\\\\vphantom{X}}\";\n              res += \"^{\"+b5.d+\"}\";\n            }\n            if (b5.q) {\n              res += \"{\\\\vphantom{X}}\";\n              res += \"_{\\\\smash[t]{\"+b5.q+\"}}\";\n            }\n          } else {\n            if (b5.q) {\n              res += \"{\\\\vphantom{X}}\";\n              res += \"_{\\\\smash[t]{\"+b5.q+\"}}\";\n            }\n            if (b5.d) {\n              res += \"{\\\\vphantom{X}}\";\n              res += \"^{\"+b5.d+\"}\";\n            }\n          }\n          break;\n        case 'rm':\n          res = \"\\\\mathrm{\"+buf.p1+\"}\";\n          break;\n        case 'text':\n          if (buf.p1.match(/[\\^_]/)) {\n            buf.p1 = buf.p1.replace(\" \", \"~\").replace(\"-\", \"\\\\text{-}\");\n            res = \"\\\\mathrm{\"+buf.p1+\"}\";\n          } else {\n            res = \"\\\\text{\"+buf.p1+\"}\";\n          }\n          break;\n        case 'roman numeral':\n          res = \"\\\\mathrm{\"+buf.p1+\"}\";\n          break;\n        case 'state of aggregation':\n          res = \"\\\\mskip2mu \"+texify._goInner(buf.p1);\n          break;\n        case 'state of aggregation subscript':\n          res = \"\\\\mskip1mu \"+texify._goInner(buf.p1);\n          break;\n        case 'bond':\n          res = texify._getBond(buf.kind_);\n          if (!res) {\n            throw [\"MhchemErrorBond\", \"mhchem Error. Unknown bond type (\" + buf.kind_ + \")\"];\n          }\n          break;\n        case 'frac':\n          var c = \"\\\\frac{\" + buf.p1 + \"}{\" + buf.p2 + \"}\";\n          res = \"\\\\mathchoice{\\\\textstyle\"+c+\"}{\"+c+\"}{\"+c+\"}{\"+c+\"}\";\n          break;\n        case 'pu-frac':\n          var d = \"\\\\frac{\" + texify._goInner(buf.p1) + \"}{\" + texify._goInner(buf.p2) + \"}\";\n          res = \"\\\\mathchoice{\\\\textstyle\"+d+\"}{\"+d+\"}{\"+d+\"}{\"+d+\"}\";\n          break;\n        case 'tex-math':\n          res = buf.p1 + \" \";\n          break;\n        case 'frac-ce':\n          res = \"\\\\frac{\" + texify._goInner(buf.p1) + \"}{\" + texify._goInner(buf.p2) + \"}\";\n          break;\n        case 'overset':\n          res = \"\\\\overset{\" + texify._goInner(buf.p1) + \"}{\" + texify._goInner(buf.p2) + \"}\";\n          break;\n        case 'underset':\n          res = \"\\\\underset{\" + texify._goInner(buf.p1) + \"}{\" + texify._goInner(buf.p2) + \"}\";\n          break;\n        case 'underbrace':\n          res =  \"\\\\underbrace{\" + texify._goInner(buf.p1) + \"}_{\" + texify._goInner(buf.p2) + \"}\";\n          break;\n        case 'color':\n          res = \"{\\\\color{\" + buf.color1 + \"}{\" + texify._goInner(buf.color2) + \"}}\";\n          break;\n        case 'color0':\n          res = \"\\\\color{\" + buf.color + \"}\";\n          break;\n        case 'arrow':\n          var b6 = {\n            rd: texify._goInner(buf.rd),\n            rq: texify._goInner(buf.rq)\n          };\n          var arrow = \"\\\\x\" + texify._getArrow(buf.r);\n          if (b6.rq) { arrow += \"[{\" + b6.rq + \"}]\"; }\n          if (b6.rd) {\n            arrow += \"{\" + b6.rd + \"}\";\n          } else {\n            arrow += \"{}\";\n          }\n          res = arrow;\n          break;\n        case 'operator':\n          res = texify._getOperator(buf.kind_);\n          break;\n        case '1st-level escape':\n          res = buf.p1+\" \";  // &, \\\\\\\\, \\\\hlin\n          break;\n        case 'space':\n          res = \" \";\n          break;\n        case 'entitySkip':\n          res = \"~\";\n          break;\n        case 'pu-space-1':\n          res = \"~\";\n          break;\n        case 'pu-space-2':\n          res = \"\\\\mkern3mu \";\n          break;\n        case '1000 separator':\n          res = \"\\\\mkern2mu \";\n          break;\n        case 'commaDecimal':\n          res = \"{,}\";\n          break;\n          case 'comma enumeration L':\n          res = \"{\"+buf.p1+\"}\\\\mkern6mu \";\n          break;\n        case 'comma enumeration M':\n          res = \"{\"+buf.p1+\"}\\\\mkern3mu \";\n          break;\n        case 'comma enumeration S':\n          res = \"{\"+buf.p1+\"}\\\\mkern1mu \";\n          break;\n        case 'hyphen':\n          res = \"\\\\text{-}\";\n          break;\n        case 'addition compound':\n          res = \"\\\\,{\\\\cdot}\\\\,\";\n          break;\n        case 'electron dot':\n          res = \"\\\\mkern1mu \\\\bullet\\\\mkern1mu \";\n          break;\n        case 'KV x':\n          res = \"{\\\\times}\";\n          break;\n        case 'prime':\n          res = \"\\\\prime \";\n          break;\n        case 'cdot':\n          res = \"\\\\cdot \";\n          break;\n        case 'tight cdot':\n          res = \"\\\\mkern1mu{\\\\cdot}\\\\mkern1mu \";\n          break;\n        case 'times':\n          res = \"\\\\times \";\n          break;\n        case 'circa':\n          res = \"{\\\\sim}\";\n          break;\n        case '^':\n          res = \"uparrow\";\n          break;\n        case 'v':\n          res = \"downarrow\";\n          break;\n        case 'ellipsis':\n          res = \"\\\\ldots \";\n          break;\n        case '/':\n          res = \"/\";\n          break;\n        case ' / ':\n          res = \"\\\\,/\\\\,\";\n          break;\n        default:\n          assertNever(buf);\n          throw [\"MhchemBugT\", \"mhchem bug T. Please report.\"];  // Missing texify rule or unknown MhchemParser output\n      }\n      assertString(res);\n      return res;\n    },\n    _getArrow: function (a) {\n      switch (a) {\n        case \"->\": return \"rightarrow\";\n        case \"\\u2192\": return \"rightarrow\";\n        case \"\\u27F6\": return \"rightarrow\";\n        case \"<-\": return \"leftarrow\";\n        case \"<->\": return \"leftrightarrow\";\n        case \"<-->\": return \"rightleftarrows\";\n        case \"<=>\": return \"rightleftharpoons\";\n        case \"\\u21CC\": return \"rightleftharpoons\";\n        case \"<=>>\": return \"rightequilibrium\";\n        case \"<<=>\": return \"leftequilibrium\";\n        default:\n          assertNever(a);\n          throw [\"MhchemBugT\", \"mhchem bug T. Please report.\"];\n      }\n    },\n    _getBond: function (a) {\n      switch (a) {\n        case \"-\": return \"{-}\";\n        case \"1\": return \"{-}\";\n        case \"=\": return \"{=}\";\n        case \"2\": return \"{=}\";\n        case \"#\": return \"{\\\\equiv}\";\n        case \"3\": return \"{\\\\equiv}\";\n        case \"~\": return \"{\\\\tripledash}\";\n        case \"~-\": return \"{\\\\mathrlap{\\\\raisebox{-.1em}{$-$}}\\\\raisebox{.1em}{$\\\\tripledash$}}\";\n        case \"~=\": return \"{\\\\mathrlap{\\\\raisebox{-.2em}{$-$}}\\\\mathrlap{\\\\raisebox{.2em}{$\\\\tripledash$}}-}\";\n        case \"~--\": return \"{\\\\mathrlap{\\\\raisebox{-.2em}{$-$}}\\\\mathrlap{\\\\raisebox{.2em}{$\\\\tripledash$}}-}\";\n        case \"-~-\": return \"{\\\\mathrlap{\\\\raisebox{-.2em}{$-$}}\\\\mathrlap{\\\\raisebox{.2em}{$-$}}\\\\tripledash}\";\n        case \"...\": return \"{{\\\\cdot}{\\\\cdot}{\\\\cdot}}\";\n        case \"....\": return \"{{\\\\cdot}{\\\\cdot}{\\\\cdot}{\\\\cdot}}\";\n        case \"->\": return \"{\\\\rightarrow}\";\n        case \"<-\": return \"{\\\\leftarrow}\";\n        case \"<\": return \"{<}\";\n        case \">\": return \"{>}\";\n        default:\n          assertNever(a);\n          throw [\"MhchemBugT\", \"mhchem bug T. Please report.\"];\n      }\n    },\n    _getOperator: function (a) {\n      switch (a) {\n        case \"+\": return \" {}+{} \";\n        case \"-\": return \" {}-{} \";\n        case \"=\": return \" {}={} \";\n        case \"<\": return \" {}<{} \";\n        case \">\": return \" {}>{} \";\n        case \"<<\": return \" {}\\\\ll{} \";\n        case \">>\": return \" {}\\\\gg{} \";\n        case \"\\\\pm\": return \" {}\\\\pm{} \";\n        case \"\\\\approx\": return \" {}\\\\approx{} \";\n        case \"$\\\\approx$\": return \" {}\\\\approx{} \";\n        case \"v\": return \" \\\\downarrow{} \";\n        case \"(v)\": return \" \\\\downarrow{} \";\n        case \"^\": return \" \\\\uparrow{} \";\n        case \"(^)\": return \" \\\\uparrow{} \";\n        default:\n          assertNever(a);\n          throw [\"MhchemBugT\", \"mhchem bug T. Please report.\"];\n      }\n    }\n  };\n\n  //\n  // Helpers for code anaylsis\n  // Will show type error at calling position\n  //\n  /** @param {number} a */\n  function assertNever(a) {}\n  /** @param {string} a */\n  function assertString(a) {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-katex/renderer.js",
    "content": "const katex = require('katex')\nconst chemParse = require('./mhchem')\n\n/* global WIKI */\n\n// ------------------------------------\n// Markdown - KaTeX Renderer\n// ------------------------------------\n//\n// Includes code from https://github.com/liradb2000/markdown-it-katex\n\n// Add \\ce, \\pu, and \\tripledash to the KaTeX macros.\nkatex.__defineMacro('\\\\ce', function(context) {\n  return chemParse(context.consumeArgs(1)[0], 'ce')\n})\nkatex.__defineMacro('\\\\pu', function(context) {\n  return chemParse(context.consumeArgs(1)[0], 'pu')\n})\n\n//  Needed for \\bond for the ~ forms\n//  Raise by 2.56mu, not 2mu. We're raising a hyphen-minus, U+002D, not\n//  a mathematical minus, U+2212. So we need that extra 0.56.\nkatex.__defineMacro('\\\\tripledash', '{\\\\vphantom{-}\\\\raisebox{2.56mu}{$\\\\mkern2mu' + '\\\\tiny\\\\text{-}\\\\mkern1mu\\\\text{-}\\\\mkern1mu\\\\text{-}\\\\mkern2mu$}}')\n\nmodule.exports = {\n  init (mdinst, conf) {\n    const macros = {}\n    if (conf.useInline) {\n      mdinst.inline.ruler.after('escape', 'katex_inline', katexInline)\n      mdinst.renderer.rules.katex_inline = (tokens, idx) => {\n        try {\n          return katex.renderToString(tokens[idx].content, {\n            displayMode: false, macros\n          })\n        } catch (err) {\n          WIKI.logger.warn(err)\n          return tokens[idx].content\n        }\n      }\n    }\n    if (conf.useBlocks) {\n      mdinst.block.ruler.after('blockquote', 'katex_block', katexBlock, {\n        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]\n      })\n      mdinst.renderer.rules.katex_block = (tokens, idx) => {\n        try {\n          return `<p>` + katex.renderToString(tokens[idx].content, {\n            displayMode: true, macros\n          }) + `</p>`\n        } catch (err) {\n          WIKI.logger.warn(err)\n          return tokens[idx].content\n        }\n      }\n    }\n  }\n}\n\n// Test if potential opening or closing delimieter\n// Assumes that there is a \"$\" at state.src[pos]\nfunction isValidDelim (state, pos) {\n  let prevChar\n  let nextChar\n  let max = state.posMax\n  let canOpen = true\n  let canClose = true\n\n  prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1\n  nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1\n\n  // Check non-whitespace conditions for opening and closing, and\n  // check that closing delimeter isn't followed by a number\n  if (prevChar === 0x20/* \" \" */ || prevChar === 0x09/* \\t */ ||\n          (nextChar >= 0x30/* \"0\" */ && nextChar <= 0x39/* \"9\" */)) {\n    canClose = false\n  }\n  if (nextChar === 0x20/* \" \" */ || nextChar === 0x09/* \\t */) {\n    canOpen = false\n  }\n\n  return {\n    canOpen: canOpen,\n    canClose: canClose\n  }\n}\n\nfunction katexInline (state, silent) {\n  let start, match, token, res, pos\n\n  if (state.src[state.pos] !== '$') { return false }\n\n  res = isValidDelim(state, state.pos)\n  if (!res.canOpen) {\n    if (!silent) { state.pending += '$' }\n    state.pos += 1\n    return true\n  }\n\n  // First check for and bypass all properly escaped delimieters\n  // This loop will assume that the first leading backtick can not\n  // be the first character in state.src, which is known since\n  // we have found an opening delimieter already.\n  start = state.pos + 1\n  match = start\n  while ((match = state.src.indexOf('$', match)) !== -1) {\n    // Found potential $, look for escapes, pos will point to\n    // first non escape when complete\n    pos = match - 1\n    while (state.src[pos] === '\\\\') { pos -= 1 }\n\n    // Even number of escapes, potential closing delimiter found\n    if (((match - pos) % 2) === 1) { break }\n    match += 1\n  }\n\n  // No closing delimter found.  Consume $ and continue.\n  if (match === -1) {\n    if (!silent) { state.pending += '$' }\n    state.pos = start\n    return true\n  }\n\n  // Check if we have empty content, ie: $$.  Do not parse.\n  if (match - start === 0) {\n    if (!silent) { state.pending += '$$' }\n    state.pos = start + 1\n    return true\n  }\n\n  // Check for valid closing delimiter\n  res = isValidDelim(state, match)\n  if (!res.canClose) {\n    if (!silent) { state.pending += '$' }\n    state.pos = start\n    return true\n  }\n\n  if (!silent) {\n    token = state.push('katex_inline', 'math', 0)\n    token.markup = '$'\n    token.content = state.src.slice(start, match)\n  }\n\n  state.pos = match + 1\n  return true\n}\n\nfunction katexBlock (state, start, end, silent) {\n  let firstLine; let lastLine; let next; let lastPos; let found = false; let token\n  let pos = state.bMarks[start] + state.tShift[start]\n  let max = state.eMarks[start]\n\n  if (pos + 2 > max) { return false }\n  if (state.src.slice(pos, pos + 2) !== '$$') { return false }\n\n  pos += 2\n  firstLine = state.src.slice(pos, max)\n\n  if (silent) { return true }\n  if (firstLine.trim().slice(-2) === '$$') {\n    // Single line expression\n    firstLine = firstLine.trim().slice(0, -2)\n    found = true\n  }\n\n  for (next = start; !found;) {\n    next++\n\n    if (next >= end) { break }\n\n    pos = state.bMarks[next] + state.tShift[next]\n    max = state.eMarks[next]\n\n    if (pos < max && state.tShift[next] < state.blkIndent) {\n      // non-empty line with negative indent should stop the list:\n      break\n    }\n\n    if (state.src.slice(pos, max).trim().slice(-2) === '$$') {\n      lastPos = state.src.slice(0, max).lastIndexOf('$$')\n      lastLine = state.src.slice(pos, lastPos)\n      found = true\n    }\n  }\n\n  state.line = next + 1\n\n  token = state.push('katex_block', 'math', 0)\n  token.block = true\n  token.content = (firstLine && firstLine.trim() ? firstLine + '\\n' : '') +\n  state.getLines(start + 1, next, state.tShift[start], true) +\n  (lastLine && lastLine.trim() ? lastLine : '')\n  token.map = [ start, state.line ]\n  token.markup = '$$'\n  return true\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-kroki/definition.yml",
    "content": "key: markdownKroki\ntitle: Kroki\ndescription: Kroki Diagrams Parser\nauthor: rlanyi (based on PlantUML renderer)\nicon: mdi-sitemap\nenabledDefault: false\ndependsOn: markdownCore\nprops:\n  server:\n    type: String\n    default: https://kroki.io\n    title: Kroki Server\n    hint: Kroki server used for image generation\n    order: 1\n    public: true\n  openMarker:\n    type: String\n    default: \"```kroki\"\n    title: Open Marker\n    hint: String to use as opening delimiter. Diagram type must be put in the next line in lowercase.\n    order: 2\n    public: true\n  closeMarker:\n    type: String\n    default: \"```\"\n    title: Close Marker\n    hint: String to use as closing delimiter\n    order: 3\n    public: true\n"
  },
  {
    "path": "server/modules/rendering/markdown-kroki/renderer.js",
    "content": "const zlib = require('zlib')\n\n// ------------------------------------\n// Markdown - Kroki Preprocessor\n// ------------------------------------\n\nmodule.exports = {\n  init (mdinst, conf) {\n    mdinst.use((md, opts) => {\n      const openMarker = opts.openMarker || '```kroki'\n      const openChar = openMarker.charCodeAt(0)\n      const closeMarker = opts.closeMarker || '```'\n      const closeChar = closeMarker.charCodeAt(0)\n      const server = opts.server || 'https://kroki.io'\n\n      md.block.ruler.before('fence', 'kroki', (state, startLine, endLine, silent) => {\n        let nextLine\n        let markup\n        let params\n        let token\n        let i\n        let autoClosed = false\n        let start = state.bMarks[startLine] + state.tShift[startLine]\n        let max = state.eMarks[startLine]\n\n        // Check out the first character quickly,\n        // this should filter out most of non-uml blocks\n        //\n        if (openChar !== state.src.charCodeAt(start)) { return false }\n\n        // Check out the rest of the marker string\n        //\n        for (i = 0; i < openMarker.length; ++i) {\n          if (openMarker[i] !== state.src[start + i]) { return false }\n        }\n\n        markup = state.src.slice(start, start + i)\n        params = state.src.slice(start + i, max)\n\n        // Since start is found, we can report success here in validation mode\n        //\n        if (silent) { return true }\n\n        // Search for the end of the block\n        //\n        nextLine = startLine\n\n        for (;;) {\n          nextLine++\n          if (nextLine >= endLine) {\n            // unclosed block should be autoclosed by end of document.\n            // also block seems to be autoclosed by end of parent\n            break\n          }\n\n          start = state.bMarks[nextLine] + state.tShift[nextLine]\n          max = state.eMarks[nextLine]\n\n          if (start < max && state.sCount[nextLine] < state.blkIndent) {\n            // non-empty line with negative indent should stop the list:\n            // - ```\n            //  test\n            break\n          }\n\n          if (closeChar !== state.src.charCodeAt(start)) {\n            // didn't find the closing fence\n            continue\n          }\n\n          if (state.sCount[nextLine] > state.sCount[startLine]) {\n            // closing fence should not be indented with respect of opening fence\n            continue\n          }\n\n          let closeMarkerMatched = true\n          for (i = 0; i < closeMarker.length; ++i) {\n            if (closeMarker[i] !== state.src[start + i]) {\n              closeMarkerMatched = false\n              break\n            }\n          }\n\n          if (!closeMarkerMatched) {\n            continue\n          }\n\n          // make sure tail has spaces only\n          if (state.skipSpaces(start + i) < max) {\n            continue\n          }\n\n          // found!\n          autoClosed = true\n          break\n        }\n\n        let contents = state.src\n          .split('\\n')\n          .slice(startLine + 1, nextLine)\n          .join('\\n')\n\n        // We generate a token list for the alt property, to mimic what the image parser does.\n        let altToken = []\n        // Remove leading space if any.\n        let alt = params ? params.slice(1) : 'uml diagram'\n        state.md.inline.parse(\n          alt,\n          state.md,\n          state.env,\n          altToken\n        )\n\n        let firstlf = contents.indexOf('\\n')\n        if (firstlf === -1) firstlf = undefined\n        let diagramType = contents.substring(0, firstlf)\n        contents = contents.substring(firstlf + 1)\n\n        let result = zlib.deflateSync(contents).toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_')\n\n        token = state.push('kroki', 'img', 0)\n        // alt is constructed from children. No point in populating it here.\n        token.attrs = [ [ 'src', `${server}/${diagramType}/svg/${result}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ]\n        token.block = true\n        token.children = altToken\n        token.info = params\n        token.map = [ startLine, nextLine ]\n        token.markup = markup\n\n        state.line = nextLine + (autoClosed ? 1 : 0)\n\n        return true\n      }, {\n        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]\n      })\n      md.renderer.rules.kroki = md.renderer.rules.image\n    }, {\n      openMarker: conf.openMarker,\n      closeMarker: conf.closeMarker,\n      server: conf.server\n    })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-mathjax/definition.yml",
    "content": "key: markdownMathjax\ntitle: Mathjax\ndescription: LaTeX Math + Chemical Expression Typesetting Renderer\nauthor: requarks.io\nicon: mdi-math-integral\nenabledDefault: false\ndependsOn: markdownCore\nprops:\n  useInline:\n    type: Boolean\n    default: true\n    title: Inline TeX\n    hint: Process inline TeX expressions surrounded by $ symbols.\n    order: 1\n  useBlocks:\n    type: Boolean\n    default: true\n    title: TeX Blocks\n    hint: Process TeX blocks enclosed by $$ symbols.\n    order: 2\n"
  },
  {
    "path": "server/modules/rendering/markdown-mathjax/renderer.js",
    "content": "const mjax = require('mathjax')\n\n/* global WIKI */\n\n// ------------------------------------\n// Markdown - MathJax Renderer\n// ------------------------------------\n\nconst extensions = [\n  'bbox',\n  'boldsymbol',\n  'braket',\n  'color',\n  'extpfeil',\n  'mhchem',\n  'newcommand',\n  'unicode',\n  'verb'\n]\n\nmodule.exports = {\n  async init (mdinst, conf) {\n    const MathJax = await mjax.init({\n      loader: {\n        require: require,\n        paths: { mathjax: 'mathjax/es5' },\n        load: [\n          'input/tex',\n          'output/svg',\n          ...extensions.map(e => `[tex]/${e}`)\n        ]\n      },\n      tex: {\n        packages: {'[+]': extensions}\n      }\n    })\n    if (conf.useInline) {\n      mdinst.inline.ruler.after('escape', 'mathjax_inline', mathjaxInline)\n      mdinst.renderer.rules.mathjax_inline = (tokens, idx) => {\n        try {\n          const result = MathJax.tex2svg(tokens[idx].content, {\n            display: false\n          })\n          return MathJax.startup.adaptor.innerHTML(result)\n        } catch (err) {\n          WIKI.logger.warn(err)\n          return tokens[idx].content\n        }\n      }\n    }\n    if (conf.useBlocks) {\n      mdinst.block.ruler.after('blockquote', 'mathjax_block', mathjaxBlock, {\n        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]\n      })\n      mdinst.renderer.rules.mathjax_block = (tokens, idx) => {\n        try {\n          const result = MathJax.tex2svg(tokens[idx].content, {\n            display: true\n          })\n          return `<p>` + MathJax.startup.adaptor.innerHTML(result) + `</p>`\n        } catch (err) {\n          WIKI.logger.warn(err)\n          return tokens[idx].content\n        }\n      }\n    }\n  }\n}\n\n// Test if potential opening or closing delimieter\n// Assumes that there is a \"$\" at state.src[pos]\nfunction isValidDelim (state, pos) {\n  let prevChar\n  let nextChar\n  let max = state.posMax\n  let canOpen = true\n  let canClose = true\n\n  prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1\n  nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1\n\n  // Check non-whitespace conditions for opening and closing, and\n  // check that closing delimeter isn't followed by a number\n  if (prevChar === 0x20/* \" \" */ || prevChar === 0x09/* \\t */ ||\n  (nextChar >= 0x30/* \"0\" */ && nextChar <= 0x39/* \"9\" */)) {\n    canClose = false\n  }\n  if (nextChar === 0x20/* \" \" */ || nextChar === 0x09/* \\t */) {\n    canOpen = false\n  }\n\n  return {\n    canOpen: canOpen,\n    canClose: canClose\n  }\n}\n\nfunction mathjaxInline (state, silent) {\n  let start, match, token, res, pos\n\n  if (state.src[state.pos] !== '$') { return false }\n\n  res = isValidDelim(state, state.pos)\n  if (!res.canOpen) {\n    if (!silent) { state.pending += '$' }\n    state.pos += 1\n    return true\n  }\n\n  // First check for and bypass all properly escaped delimieters\n  // This loop will assume that the first leading backtick can not\n  // be the first character in state.src, which is known since\n  // we have found an opening delimieter already.\n  start = state.pos + 1\n  match = start\n  while ((match = state.src.indexOf('$', match)) !== -1) {\n    // Found potential $, look for escapes, pos will point to\n    // first non escape when complete\n    pos = match - 1\n    while (state.src[pos] === '\\\\') { pos -= 1 }\n\n    // Even number of escapes, potential closing delimiter found\n    if (((match - pos) % 2) === 1) { break }\n    match += 1\n  }\n\n  // No closing delimter found.  Consume $ and continue.\n  if (match === -1) {\n    if (!silent) { state.pending += '$' }\n    state.pos = start\n    return true\n  }\n\n  // Check if we have empty content, ie: $$.  Do not parse.\n  if (match - start === 0) {\n    if (!silent) { state.pending += '$$' }\n    state.pos = start + 1\n    return true\n  }\n\n  // Check for valid closing delimiter\n  res = isValidDelim(state, match)\n  if (!res.canClose) {\n    if (!silent) { state.pending += '$' }\n    state.pos = start\n    return true\n  }\n\n  if (!silent) {\n    token = state.push('mathjax_inline', 'math', 0)\n    token.markup = '$'\n    token.content = state.src.slice(start, match)\n  }\n\n  state.pos = match + 1\n  return true\n}\n\nfunction mathjaxBlock (state, start, end, silent) {\n  let firstLine; let lastLine; let next; let lastPos; let found = false; let token\n  let pos = state.bMarks[start] + state.tShift[start]\n  let max = state.eMarks[start]\n\n  if (pos + 2 > max) { return false }\n  if (state.src.slice(pos, pos + 2) !== '$$') { return false }\n\n  pos += 2\n  firstLine = state.src.slice(pos, max)\n\n  if (silent) { return true }\n  if (firstLine.trim().slice(-2) === '$$') {\n    // Single line expression\n    firstLine = firstLine.trim().slice(0, -2)\n    found = true\n  }\n\n  for (next = start; !found;) {\n    next++\n\n    if (next >= end) { break }\n\n    pos = state.bMarks[next] + state.tShift[next]\n    max = state.eMarks[next]\n\n    if (pos < max && state.tShift[next] < state.blkIndent) {\n      // non-empty line with negative indent should stop the list:\n      break\n    }\n\n    if (state.src.slice(pos, max).trim().slice(-2) === '$$') {\n      lastPos = state.src.slice(0, max).lastIndexOf('$$')\n      lastLine = state.src.slice(pos, lastPos)\n      found = true\n    }\n  }\n\n  state.line = next + 1\n\n  token = state.push('mathjax_block', 'math', 0)\n  token.block = true\n  token.content = (firstLine && firstLine.trim() ? firstLine + '\\n' : '') +\n  state.getLines(start + 1, next, state.tShift[start], true) +\n  (lastLine && lastLine.trim() ? lastLine : '')\n  token.map = [ start, state.line ]\n  token.markup = '$$'\n  return true\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-multi-table/definition.yml",
    "content": "key: markdownMultiTable\ntitle: MultiMarkdown Table\ndescription: Add MultiMarkdown table support\nauthor: requarks.io\nicon: mdi-table\nenabledDefault: false\ndependsOn: markdownCore\nprops:\n  multilineEnabled:\n    type: Boolean\n    title: Multiline\n    hint: Enable multiple lines rows\n    default: true\n  headerlessEnabled:\n    type: Boolean\n    title: Headerless\n    hint: Enable ommited table headers\n    default: true\n  rowspanEnabled:\n    type: Boolean\n    title: Rowspan\n    hint: Enable table row spans\n    default: true\n"
  },
  {
    "path": "server/modules/rendering/markdown-multi-table/renderer.js",
    "content": "const multiTable = require('markdown-it-multimd-table')\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(multiTable, {\n      multiline: conf.multilineEnabled,\n      rowspan: conf.rowspanEnabled,\n      headerless: conf.headerlessEnabled\n    })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-pivot-table/definition.yml",
    "content": "key: markdownPivotTable\ntitle: Pivot Table\ndescription: Add pivot table support\nauthor: jaeseopark\nicon: mdi-table\nenabledDefault: false\ndependsOn: markdownCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-pivot-table/renderer.js",
    "content": "const pivotTable = require('markdown-it-pivot-table')\n\nmodule.exports = {\n  init (md) {\n    md.use(pivotTable)\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-plantuml/definition.yml",
    "content": "key: markdownPlantuml\ntitle: PlantUML\ndescription: PlantUML Markdown Parser\nauthor: ethanmdavidson\nicon: mdi-sitemap\nenabledDefault: true\ndependsOn: markdownCore\nprops:\n  server:\n    type: String\n    default: https://plantuml.requarks.io\n    title: PlantUML Server\n    hint: PlantUML server used for image generation\n    order: 1\n    public: true\n  openMarker:\n    type: String\n    default: \"```plantuml\"\n    title: Open Marker\n    hint: String to use as opening delimiter\n    order: 2\n    public: true\n  closeMarker:\n    type: String\n    default: \"```\"\n    title: Close Marker\n    hint: String to use as closing delimiter\n    order: 3\n    public: true\n  imageFormat:\n    type: String\n    default: svg\n    title: Image Format\n    hint: Format to use for rendered PlantUML images\n    enum:\n      - svg\n      - png\n      - latex\n      - ascii\n    order: 4\n    public: true\n"
  },
  {
    "path": "server/modules/rendering/markdown-plantuml/renderer.js",
    "content": "const zlib = require('zlib')\n\n// ------------------------------------\n// Markdown - PlantUML Preprocessor\n// ------------------------------------\n\nmodule.exports = {\n  init (mdinst, conf) {\n    mdinst.use((md, opts) => {\n      const openMarker = opts.openMarker || '```plantuml'\n      const openChar = openMarker.charCodeAt(0)\n      const closeMarker = opts.closeMarker || '```'\n      const closeChar = closeMarker.charCodeAt(0)\n      const imageFormat = opts.imageFormat || 'svg'\n      const server = opts.server || 'https://plantuml.requarks.io'\n\n      md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {\n        let nextLine\n        let markup\n        let params\n        let token\n        let i\n        let autoClosed = false\n        let start = state.bMarks[startLine] + state.tShift[startLine]\n        let max = state.eMarks[startLine]\n\n        // Check out the first character quickly,\n        // this should filter out most of non-uml blocks\n        //\n        if (openChar !== state.src.charCodeAt(start)) { return false }\n\n        // Check out the rest of the marker string\n        //\n        for (i = 0; i < openMarker.length; ++i) {\n          if (openMarker[i] !== state.src[start + i]) { return false }\n        }\n\n        markup = state.src.slice(start, start + i)\n        params = state.src.slice(start + i, max)\n\n        // Since start is found, we can report success here in validation mode\n        //\n        if (silent) { return true }\n\n        // Search for the end of the block\n        //\n        nextLine = startLine\n\n        for (;;) {\n          nextLine++\n          if (nextLine >= endLine) {\n            // unclosed block should be autoclosed by end of document.\n            // also block seems to be autoclosed by end of parent\n            break\n          }\n\n          start = state.bMarks[nextLine] + state.tShift[nextLine]\n          max = state.eMarks[nextLine]\n\n          if (start < max && state.sCount[nextLine] < state.blkIndent) {\n            // non-empty line with negative indent should stop the list:\n            // - ```\n            //  test\n            break\n          }\n\n          if (closeChar !== state.src.charCodeAt(start)) {\n            // didn't find the closing fence\n            continue\n          }\n\n          if (state.sCount[nextLine] > state.sCount[startLine]) {\n            // closing fence should not be indented with respect of opening fence\n            continue\n          }\n\n          let closeMarkerMatched = true\n          for (i = 0; i < closeMarker.length; ++i) {\n            if (closeMarker[i] !== state.src[start + i]) {\n              closeMarkerMatched = false\n              break\n            }\n          }\n\n          if (!closeMarkerMatched) {\n            continue\n          }\n\n          // make sure tail has spaces only\n          if (state.skipSpaces(start + i) < max) {\n            continue\n          }\n\n          // found!\n          autoClosed = true\n          break\n        }\n\n        const contents = state.src\n          .split('\\n')\n          .slice(startLine + 1, nextLine)\n          .join('\\n')\n\n        // We generate a token list for the alt property, to mimic what the image parser does.\n        let altToken = []\n        // Remove leading space if any.\n        let alt = params ? params.slice(1) : 'uml diagram'\n        state.md.inline.parse(\n          alt,\n          state.md,\n          state.env,\n          altToken\n        )\n\n        const zippedCode = encode64(zlib.deflateRawSync('@startuml\\n' + contents + '\\n@enduml').toString('binary'))\n\n        token = state.push('uml_diagram', 'img', 0)\n        // alt is constructed from children. No point in populating it here.\n        token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ]\n        token.block = true\n        token.children = altToken\n        token.info = params\n        token.map = [ startLine, nextLine ]\n        token.markup = markup\n\n        state.line = nextLine + (autoClosed ? 1 : 0)\n\n        return true\n      }, {\n        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]\n      })\n      md.renderer.rules.uml_diagram = md.renderer.rules.image\n    }, {\n      openMarker: conf.openMarker,\n      closeMarker: conf.closeMarker,\n      imageFormat: conf.imageFormat,\n      server: conf.server\n    })\n  }\n}\n\nfunction encode64 (data) {\n  let r = ''\n  for (let i = 0; i < data.length; i += 3) {\n    if (i + 2 === data.length) {\n      r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)\n    } else if (i + 1 === data.length) {\n      r += append3bytes(data.charCodeAt(i), 0, 0)\n    } else {\n      r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))\n    }\n  }\n  return r\n}\n\nfunction append3bytes (b1, b2, b3) {\n  let c1 = b1 >> 2\n  let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)\n  let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)\n  let c4 = b3 & 0x3F\n  let r = ''\n  r += encode6bit(c1 & 0x3F)\n  r += encode6bit(c2 & 0x3F)\n  r += encode6bit(c3 & 0x3F)\n  r += encode6bit(c4 & 0x3F)\n  return r\n}\n\nfunction encode6bit(raw) {\n  let b = raw\n  if (b < 10) {\n    return String.fromCharCode(48 + b)\n  }\n  b -= 10\n  if (b < 26) {\n    return String.fromCharCode(65 + b)\n  }\n  b -= 26\n  if (b < 26) {\n    return String.fromCharCode(97 + b)\n  }\n  b -= 26\n  if (b === 0) {\n    return '-'\n  }\n  if (b === 1) {\n    return '_'\n  }\n  return '?'\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-supsub/definition.yml",
    "content": "key: markdownSupsub\ntitle: Subscript/Superscript\ndescription: Parse subscript and superscript tags\nauthor: requarks.io\nicon: mdi-format-superscript\nenabledDefault: true\ndependsOn: markdownCore\nprops:\n  subEnabled:\n    type: Boolean\n    title: Subscript\n    hint: Enable subscript tags\n    default: true\n  supEnabled:\n    type: Boolean\n    title: Superscript\n    hint: Enable superscript tags\n    default: true\n"
  },
  {
    "path": "server/modules/rendering/markdown-supsub/renderer.js",
    "content": "const mdSub = require('markdown-it-sub')\nconst mdSup = require('markdown-it-sup')\n\n// ------------------------------------\n// Markdown - Subscript / Superscript\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    if (conf.subEnabled) {\n      md.use(mdSub)\n    }\n    if (conf.supEnabled) {\n      md.use(mdSup)\n    }\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/markdown-tasklists/definition.yml",
    "content": "key: markdownTasklists\ntitle: Task Lists\ndescription: Parse task lists to checkboxes\nauthor: requarks.io\nicon: mdi-format-list-checks\nenabledDefault: true\ndependsOn: markdownCore\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/markdown-tasklists/renderer.js",
    "content": "const mdTaskLists = require('markdown-it-task-lists')\n\n// ------------------------------------\n// Markdown - Task Lists\n// ------------------------------------\n\nmodule.exports = {\n  init (md, conf) {\n    md.use(mdTaskLists, { label: false, labelAfter: false })\n  }\n}\n"
  },
  {
    "path": "server/modules/rendering/openapi-core/definition.yml",
    "content": "key: openapiCore\ntitle: Core\ndescription: Basic OpenAPI Parser\nauthor: requarks.io\ninput: openapi\noutput: html\nicon: mdi-api\nprops: {}\n"
  },
  {
    "path": "server/modules/rendering/openapi-core/renderer.js",
    "content": "const _ = require('lodash')\n\nmodule.exports = {\n  async render() {\n    let output = this.input\n\n    for (let child of this.children) {\n      const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)\n      output = await renderer.init(output, child.config)\n    }\n\n    return output\n  }\n}\n"
  },
  {
    "path": "server/modules/search/algolia/definition.yml",
    "content": "key: algolia\ntitle: Algolia\ndescription: Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/algolia.svg\nwebsite: https://www.algolia.com/\nisAvailable: true\nprops:\n  appId:\n    type: String\n    title: App ID\n    hint: Your Algolia Application ID, found under API Keys\n    order: 1\n  apiKey:\n    type: String\n    title: Admin API Key\n    hint: Your Algolia Admin API Key, found under API Keys.\n    order: 2\n  indexName:\n    type: String\n    title: Index Name\n    hint: The name of the index you created under Indices.\n    default: wiki\n    order: 3\n"
  },
  {
    "path": "server/modules/search/algolia/engine.js",
    "content": "const _ = require('lodash')\nconst algoliasearch = require('algoliasearch')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\n\n/* global WIKI */\n\nmodule.exports = {\n  async activate() {\n    // not used\n  },\n  async deactivate() {\n    // not used\n  },\n  /**\n   * INIT\n   */\n  async init() {\n    WIKI.logger.info(`(SEARCH/ALGOLIA) Initializing...`)\n    this.client = algoliasearch(this.config.appId, this.config.apiKey)\n    this.index = this.client.initIndex(this.config.indexName)\n\n    // -> Create Search Index\n    WIKI.logger.info(`(SEARCH/ALGOLIA) Setting index configuration...`)\n    await this.index.setSettings({\n      searchableAttributes: [\n        'title',\n        'description',\n        'content'\n      ],\n      attributesToRetrieve: [\n        'locale',\n        'path',\n        'title',\n        'description'\n      ],\n      advancedSyntax: true\n    })\n    WIKI.logger.info(`(SEARCH/ALGOLIA) Initialization completed.`)\n  },\n  /**\n   * QUERY\n   *\n   * @param {String} q Query\n   * @param {Object} opts Additional options\n   */\n  async query(q, opts) {\n    try {\n      const results = await this.index.search(q, {\n        hitsPerPage: 50\n      })\n      return {\n        results: _.map(results.hits, r => ({\n          id: r.objectID,\n          locale: r.locale,\n          path: r.path,\n          title: r.title,\n          description: r.description\n        })),\n        suggestions: [],\n        totalHits: results.nbHits\n      }\n    } catch (err) {\n      WIKI.logger.warn('Search Engine Error:')\n      WIKI.logger.warn(err)\n    }\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    await this.index.saveObject({\n      objectID: page.hash,\n      locale: page.localeCode,\n      path: page.path,\n      title: page.title,\n      description: page.description,\n      content: page.safeContent\n    })\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    await this.index.partialUpdateObject({\n      objectID: page.hash,\n      title: page.title,\n      description: page.description,\n      content: page.safeContent\n    })\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    await this.index.deleteObject(page.hash)\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    await this.index.deleteObject(page.hash)\n    await this.index.saveObject({\n      objectID: page.destinationHash,\n      locale: page.destinationLocaleCode,\n      path: page.destinationPath,\n      title: page.title,\n      description: page.description,\n      content: page.safeContent\n    })\n  },\n  /**\n   * REBUILD INDEX\n   */\n  async rebuild() {\n    WIKI.logger.info(`(SEARCH/ALGOLIA) Rebuilding Index...`)\n    await this.index.clearObjects()\n\n    const MAX_DOCUMENT_BYTES = 10 * Math.pow(2, 10) // 10 KB\n    const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB\n    const MAX_INDEXING_COUNT = 1000\n    const COMMA_BYTES = Buffer.from(',').byteLength\n\n    let chunks = []\n    let bytes = 0\n\n    const processDocument = async (cb, doc) => {\n      try {\n        if (doc) {\n          const docBytes = Buffer.from(JSON.stringify(doc)).byteLength\n          // -> Document too large\n          if (docBytes >= MAX_DOCUMENT_BYTES) {\n            throw new Error('Document exceeds maximum size allowed by Algolia.')\n          }\n\n          // -> Current batch exceeds size hard limit, flush\n          if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) {\n            await flushBuffer()\n          }\n\n          if (chunks.length > 0) {\n            bytes += COMMA_BYTES\n          }\n          bytes += docBytes\n          chunks.push(doc)\n\n          // -> Current batch exceeds count soft limit, flush\n          if (chunks.length >= MAX_INDEXING_COUNT) {\n            await flushBuffer()\n          }\n        } else {\n          // -> End of stream, flush\n          await flushBuffer()\n        }\n        cb()\n      } catch (err) {\n        cb(err)\n      }\n    }\n\n    const flushBuffer = async () => {\n      WIKI.logger.info(`(SEARCH/ALGOLIA) Sending batch of ${chunks.length}...`)\n      try {\n        await this.index.saveObjects(\n          _.map(chunks, doc => ({\n            objectID: doc.id,\n            locale: doc.locale,\n            path: doc.path,\n            title: doc.title,\n            description: doc.description,\n            content: WIKI.models.pages.cleanHTML(doc.render)\n          }))\n        )\n      } catch (err) {\n        WIKI.logger.warn('(SEARCH/ALGOLIA) Failed to send batch to Algolia: ', err)\n      }\n      chunks.length = 0\n      bytes = 0\n    }\n\n    await pipeline(\n      WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({\n        isPublished: true,\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (chunk, enc, cb) => processDocument(cb, chunk),\n        flush: async (cb) => processDocument(cb)\n      })\n    )\n    WIKI.logger.info(`(SEARCH/ALGOLIA) Index rebuilt successfully.`)\n  }\n}\n"
  },
  {
    "path": "server/modules/search/aws/definition.yml",
    "content": "key: aws\ntitle: AWS CloudSearch\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/aws-cloudsearch.svg\nwebsite: https://aws.amazon.com/cloudsearch/\nisAvailable: true\nprops:\n  domain:\n    type: String\n    title: Search Domain\n    hint: The name of your CloudSearch service.\n    order: 1\n  endpoint:\n    type: String\n    title: Document Endpoint\n    hint: The Document Endpoint specified in the domain AWS console dashboard.\n    order: 2\n  region:\n    type: String\n    title: Region\n    hint: The AWS datacenter region where the instance was created.\n    default: us-east-1\n    enum:\n      - ap-northeast-1\n      - ap-northeast-2\n      - ap-southeast-1\n      - ap-southeast-2\n      - eu-central-1\n      - eu-west-1\n      - sa-east-1\n      - us-east-1\n      - us-west-1\n      - us-west-2\n    order: 3\n  accessKeyId:\n    type: String\n    title: Access Key ID\n    hint: The Access Key ID with CloudSearchFullAccess role access to the CloudSearch instance.\n    order: 4\n  secretAccessKey :\n    type: String\n    title: Secret Access Key\n    hint: The Secret Access Key for the Access Key ID provided above.\n    order: 5\n  AnalysisSchemeLang:\n    type: String\n    title: Analysis Scheme Language\n    hint: The language used to analyse content.\n    default: en\n    enum:\n      - 'ar'\n      - 'bg'\n      - 'ca'\n      - 'cs'\n      - 'da'\n      - 'de'\n      - 'el'\n      - 'en'\n      - 'es'\n      - 'eu'\n      - 'fa'\n      - 'fi'\n      - 'fr'\n      - 'ga'\n      - 'gl'\n      - 'he'\n      - 'hi'\n      - 'hu'\n      - 'hy'\n      - 'id'\n      - 'it'\n      - 'ja'\n      - 'ko'\n      - 'lv'\n      - 'mul'\n      - 'nl'\n      - 'no'\n      - 'pt'\n      - 'ro'\n      - 'ru'\n      - 'sv'\n      - 'th'\n      - 'tr'\n      - 'zh-Hans'\n      - 'zh-Hant'\n    order: 6\n\n"
  },
  {
    "path": "server/modules/search/aws/engine.js",
    "content": "const _ = require('lodash')\nconst AWS = require('aws-sdk')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\n\n/* global WIKI */\n\nmodule.exports = {\n  async activate() {\n    // not used\n  },\n  async deactivate() {\n    // not used\n  },\n  /**\n   * INIT\n   */\n  async init() {\n    WIKI.logger.info(`(SEARCH/AWS) Initializing...`)\n    this.client = new AWS.CloudSearch({\n      apiVersion: '2013-01-01',\n      accessKeyId: this.config.accessKeyId,\n      secretAccessKey: this.config.secretAccessKey,\n      region: this.config.region\n    })\n    this.clientDomain = new AWS.CloudSearchDomain({\n      apiVersion: '2013-01-01',\n      endpoint: this.config.endpoint,\n      accessKeyId: this.config.accessKeyId,\n      secretAccessKey: this.config.secretAccessKey,\n      region: this.config.region\n    })\n\n    let rebuildIndex = false\n\n    // -> Define Analysis Schemes\n    const schemes = await this.client.describeAnalysisSchemes({\n      DomainName: this.config.domain,\n      AnalysisSchemeNames: ['default_anlscheme']\n    }).promise()\n    if (_.get(schemes, 'AnalysisSchemes', []).length < 1) {\n      WIKI.logger.info(`(SEARCH/AWS) Defining Analysis Scheme...`)\n      await this.client.defineAnalysisScheme({\n        DomainName: this.config.domain,\n        AnalysisScheme: {\n          AnalysisSchemeLanguage: this.config.AnalysisSchemeLang,\n          AnalysisSchemeName: 'default_anlscheme'\n        }\n      }).promise()\n      rebuildIndex = true\n    }\n\n    // -> Define Index Fields\n    const fields = await this.client.describeIndexFields({\n      DomainName: this.config.domain\n    }).promise()\n    if (_.get(fields, 'IndexFields', []).length < 1) {\n      WIKI.logger.info(`(SEARCH/AWS) Defining Index Fields...`)\n      await this.client.defineIndexField({\n        DomainName: this.config.domain,\n        IndexField: {\n          IndexFieldName: 'id',\n          IndexFieldType: 'literal'\n        }\n      }).promise()\n      await this.client.defineIndexField({\n        DomainName: this.config.domain,\n        IndexField: {\n          IndexFieldName: 'path',\n          IndexFieldType: 'literal'\n        }\n      }).promise()\n      await this.client.defineIndexField({\n        DomainName: this.config.domain,\n        IndexField: {\n          IndexFieldName: 'locale',\n          IndexFieldType: 'literal'\n        }\n      }).promise()\n      await this.client.defineIndexField({\n        DomainName: this.config.domain,\n        IndexField: {\n          IndexFieldName: 'title',\n          IndexFieldType: 'text',\n          TextOptions: {\n            ReturnEnabled: true,\n            AnalysisScheme: 'default_anlscheme'\n          }\n        }\n      }).promise()\n      await this.client.defineIndexField({\n        DomainName: this.config.domain,\n        IndexField: {\n          IndexFieldName: 'description',\n          IndexFieldType: 'text',\n          TextOptions: {\n            ReturnEnabled: true,\n            AnalysisScheme: 'default_anlscheme'\n          }\n        }\n      }).promise()\n      await this.client.defineIndexField({\n        DomainName: this.config.domain,\n        IndexField: {\n          IndexFieldName: 'content',\n          IndexFieldType: 'text',\n          TextOptions: {\n            ReturnEnabled: false,\n            AnalysisScheme: 'default_anlscheme'\n          }\n        }\n      }).promise()\n      rebuildIndex = true\n    }\n\n    // -> Define suggester\n    const suggesters = await this.client.describeSuggesters({\n      DomainName: this.config.domain,\n      SuggesterNames: ['default_suggester']\n    }).promise()\n    if (_.get(suggesters, 'Suggesters', []).length < 1) {\n      WIKI.logger.info(`(SEARCH/AWS) Defining Suggester...`)\n      await this.client.defineSuggester({\n        DomainName: this.config.domain,\n        Suggester: {\n          SuggesterName: 'default_suggester',\n          DocumentSuggesterOptions: {\n            SourceField: 'title',\n            FuzzyMatching: 'high'\n          }\n        }\n      }).promise()\n      rebuildIndex = true\n    }\n\n    // -> Rebuild Index\n    if (rebuildIndex) {\n      WIKI.logger.info(`(SEARCH/AWS) Requesting Index Rebuild...`)\n      await this.client.indexDocuments({\n        DomainName: this.config.domain\n      }).promise()\n    }\n\n    WIKI.logger.info(`(SEARCH/AWS) Initialization completed.`)\n  },\n  /**\n   * QUERY\n   *\n   * @param {String} q Query\n   * @param {Object} opts Additional options\n   */\n  async query(q, opts) {\n    try {\n      let suggestions = []\n      const results = await this.clientDomain.search({\n        query: q,\n        partial: true,\n        size: 50\n      }).promise()\n      if (results.hits.found < 5) {\n        const suggestResults = await this.clientDomain.suggest({\n          query: q,\n          suggester: 'default_suggester',\n          size: 5\n        }).promise()\n        suggestions = suggestResults.suggest.suggestions.map(s => s.suggestion)\n      }\n      return {\n        results: _.map(results.hits.hit, r => ({\n          id: r.id,\n          path: _.head(r.fields.path),\n          locale: _.head(r.fields.locale),\n          title: _.head(r.fields.title) || '',\n          description: _.head(r.fields.description) || ''\n        })),\n        suggestions: suggestions,\n        totalHits: results.hits.found\n      }\n    } catch (err) {\n      WIKI.logger.warn('Search Engine Error:')\n      WIKI.logger.warn(err)\n    }\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    await this.clientDomain.uploadDocuments({\n      contentType: 'application/json',\n      documents: JSON.stringify([\n        {\n          type: 'add',\n          id: page.hash,\n          fields: {\n            locale: page.localeCode,\n            path: page.path,\n            title: page.title,\n            description: page.description,\n            content: page.safeContent\n          }\n        }\n      ])\n    }).promise()\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    await this.clientDomain.uploadDocuments({\n      contentType: 'application/json',\n      documents: JSON.stringify([\n        {\n          type: 'add',\n          id: page.hash,\n          fields: {\n            locale: page.localeCode,\n            path: page.path,\n            title: page.title,\n            description: page.description,\n            content: page.safeContent\n          }\n        }\n      ])\n    }).promise()\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    await this.clientDomain.uploadDocuments({\n      contentType: 'application/json',\n      documents: JSON.stringify([\n        {\n          type: 'delete',\n          id: page.hash\n        }\n      ])\n    }).promise()\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    await this.clientDomain.uploadDocuments({\n      contentType: 'application/json',\n      documents: JSON.stringify([\n        {\n          type: 'delete',\n          id: page.hash\n        }\n      ])\n    }).promise()\n    await this.clientDomain.uploadDocuments({\n      contentType: 'application/json',\n      documents: JSON.stringify([\n        {\n          type: 'add',\n          id: page.destinationHash,\n          fields: {\n            locale: page.destinationLocaleCode,\n            path: page.destinationPath,\n            title: page.title,\n            description: page.description,\n            content: page.safeContent\n          }\n        }\n      ])\n    }).promise()\n  },\n  /**\n   * REBUILD INDEX\n   */\n  async rebuild() {\n    WIKI.logger.info(`(SEARCH/AWS) Rebuilding Index...`)\n\n    const MAX_DOCUMENT_BYTES = Math.pow(2, 20)\n    const MAX_INDEXING_BYTES = 5 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength\n    const MAX_INDEXING_COUNT = 1000\n    const COMMA_BYTES = Buffer.from(',').byteLength\n\n    let chunks = []\n    let bytes = 0\n\n    const processDocument = async (cb, doc) => {\n      try {\n        if (doc) {\n          const docBytes = Buffer.from(JSON.stringify(doc)).byteLength\n          // -> Document too large\n          if (docBytes >= MAX_DOCUMENT_BYTES) {\n            throw new Error('Document exceeds maximum size allowed by AWS CloudSearch.')\n          }\n\n          // -> Current batch exceeds size hard limit, flush\n          if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) {\n            await flushBuffer()\n          }\n\n          if (chunks.length > 0) {\n            bytes += COMMA_BYTES\n          }\n          bytes += docBytes\n          chunks.push(doc)\n\n          // -> Current batch exceeds count soft limit, flush\n          if (chunks.length >= MAX_INDEXING_COUNT) {\n            await flushBuffer()\n          }\n        } else {\n          // -> End of stream, flush\n          await flushBuffer()\n        }\n        cb()\n      } catch (err) {\n        cb(err)\n      }\n    }\n\n    const flushBuffer = async () => {\n      WIKI.logger.info(`(SEARCH/AWS) Sending batch of ${chunks.length}...`)\n      try {\n        await this.clientDomain.uploadDocuments({\n          contentType: 'application/json',\n          documents: JSON.stringify(_.map(chunks, doc => ({\n            type: 'add',\n            id: doc.id,\n            fields: {\n              locale: doc.locale,\n              path: doc.path,\n              title: doc.title,\n              description: doc.description,\n              content: WIKI.models.pages.cleanHTML(doc.render)\n            }\n          })))\n        }).promise()\n      } catch (err) {\n        WIKI.logger.warn('(SEARCH/AWS) Failed to send batch to AWS CloudSearch: ', err)\n      }\n      chunks.length = 0\n      bytes = 0\n    }\n\n    await pipeline(\n      WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({\n        isPublished: true,\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (chunk, enc, cb) => processDocument(cb, chunk),\n        flush: async (cb) => processDocument(cb)\n      })\n    )\n\n    WIKI.logger.info(`(SEARCH/AWS) Requesting Index Rebuild...`)\n    await this.client.indexDocuments({\n      DomainName: this.config.domain\n    }).promise()\n\n    WIKI.logger.info(`(SEARCH/AWS) Index rebuilt successfully.`)\n  }\n}\n"
  },
  {
    "path": "server/modules/search/azure/definition.yml",
    "content": "key: azure\ntitle: Azure Search\ndescription: AI-Powered cloud search service for web and mobile app development.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/azure.svg\nwebsite: https://azure.microsoft.com/services/search/\nisAvailable: true\nprops:\n  serviceName:\n    type: String\n    title: Service Name\n    hint: The name of the Azure Search Service. Found under Properties.\n    order: 1\n  adminKey:\n    type: String\n    title: Admin API Key\n    hint: Either the primary or secondary admin key. Found under Keys.\n    order: 2\n  indexName:\n    type: String\n    title: Index Name\n    hint: 'Name to use when creating the index. (default: wiki)'\n    default: wiki\n    order: 3\n"
  },
  {
    "path": "server/modules/search/azure/engine.js",
    "content": "const _ = require('lodash')\nconst { SearchService, QueryType } = require('azure-search-client')\nconst request = require('request-promise')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\n\n/* global WIKI */\n\nmodule.exports = {\n  async activate() {\n    // not used\n  },\n  async deactivate() {\n    // not used\n  },\n  /**\n   * INIT\n   */\n  async init() {\n    WIKI.logger.info(`(SEARCH/AZURE) Initializing...`)\n    this.client = new SearchService(this.config.serviceName, this.config.adminKey)\n\n    // -> Create Search Index\n    const indexes = await this.client.indexes.list()\n    if (!_.find(_.get(indexes, 'result.value', []), ['name', this.config.indexName])) {\n      WIKI.logger.info(`(SEARCH/AZURE) Creating index...`)\n      await this.client.indexes.create({\n        name: this.config.indexName,\n        fields: [\n          {\n            name: 'id',\n            type: 'Edm.String',\n            key: true,\n            searchable: false\n          },\n          {\n            name: 'locale',\n            type: 'Edm.String',\n            searchable: false\n          },\n          {\n            name: 'path',\n            type: 'Edm.String',\n            searchable: false\n          },\n          {\n            name: 'title',\n            type: 'Edm.String',\n            searchable: true\n          },\n          {\n            name: 'description',\n            type: 'Edm.String',\n            searchable: true\n          },\n          {\n            name: 'content',\n            type: 'Edm.String',\n            searchable: true\n          }\n        ],\n        scoringProfiles: [\n          {\n            name: 'fieldWeights',\n            text: {\n              weights: {\n                title: 4,\n                description: 3,\n                content: 1\n              }\n            }\n          }\n        ],\n        suggesters: [\n          {\n            name: 'suggestions',\n            searchMode: 'analyzingInfixMatching',\n            sourceFields: ['title', 'description', 'content']\n          }\n        ]\n      })\n    }\n    WIKI.logger.info(`(SEARCH/AZURE) Initialization completed.`)\n  },\n  /**\n   * QUERY\n   *\n   * @param {String} q Query\n   * @param {Object} opts Additional options\n   */\n  async query(q, opts) {\n    try {\n      let suggestions = []\n      const results = await this.client.indexes.use(this.config.indexName).search({\n        count: true,\n        scoringProfile: 'fieldWeights',\n        search: q,\n        select: 'id, locale, path, title, description',\n        queryType: QueryType.simple,\n        top: 50\n      })\n      if (results.result.value.length < 5) {\n        // Using plain request, not yet available in library...\n        try {\n          const suggestResults = await request({\n            uri: `https://${this.config.serviceName}.search.windows.net/indexes/${this.config.indexName}/docs/autocomplete`,\n            method: 'post',\n            qs: {\n              'api-version': '2017-11-11-Preview'\n            },\n            headers: {\n              'api-key': this.config.adminKey,\n              'Content-Type': 'application/json'\n            },\n            json: true,\n            body: {\n              autocompleteMode: 'oneTermWithContext',\n              search: q,\n              suggesterName: 'suggestions'\n            }\n          })\n          suggestions = suggestResults.value.map(s => s.queryPlusText)\n        } catch (err) {\n          WIKI.logger.warn('Search Engine suggestion failure: ', err)\n        }\n      }\n      return {\n        results: results.result.value,\n        suggestions,\n        totalHits: results.result['@odata.count']\n      }\n    } catch (err) {\n      WIKI.logger.warn('Search Engine Error:')\n      WIKI.logger.warn(err)\n    }\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    await this.client.indexes.use(this.config.indexName).index([\n      {\n        id: page.hash,\n        locale: page.localeCode,\n        path: page.path,\n        title: page.title,\n        description: page.description,\n        content: page.safeContent\n      }\n    ])\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    await this.client.indexes.use(this.config.indexName).index([\n      {\n        id: page.hash,\n        locale: page.localeCode,\n        path: page.path,\n        title: page.title,\n        description: page.description,\n        content: page.safeContent\n      }\n    ])\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    await this.client.indexes.use(this.config.indexName).index([\n      {\n        '@search.action': 'delete',\n        id: page.hash\n      }\n    ])\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    await this.client.indexes.use(this.config.indexName).index([\n      {\n        '@search.action': 'delete',\n        id: page.hash\n      }\n    ])\n    await this.client.indexes.use(this.config.indexName).index([\n      {\n        id: page.destinationHash,\n        locale: page.destinationLocaleCode,\n        path: page.destinationPath,\n        title: page.title,\n        description: page.description,\n        content: page.safeContent\n      }\n    ])\n  },\n  /**\n   * REBUILD INDEX\n   */\n  async rebuild() {\n    WIKI.logger.info(`(SEARCH/AZURE) Rebuilding Index...`)\n    await pipeline(\n      WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({\n        isPublished: true,\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: (chunk, enc, cb) => {\n          cb(null, {\n            id: chunk.id,\n            path: chunk.path,\n            locale: chunk.locale,\n            title: chunk.title,\n            description: chunk.description,\n            content: WIKI.models.pages.cleanHTML(chunk.render)\n          })\n        }\n      }),\n      this.client.indexes.use(this.config.indexName).createIndexingStream()\n    )\n    WIKI.logger.info(`(SEARCH/AZURE) Index rebuilt successfully.`)\n  }\n}\n"
  },
  {
    "path": "server/modules/search/db/definition.yml",
    "content": "key: db\ntitle: Database - Basic\ndescription: Default basic database-based search engine.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/database.svg\nwebsite: https://www.requarks.io/\nisAvailable: true\nprops: {}\n"
  },
  {
    "path": "server/modules/search/db/engine.js",
    "content": "/* global WIKI */\n\nmodule.exports = {\n  activate() {\n    // not used\n  },\n  deactivate() {\n    // not used\n  },\n  /**\n   * INIT\n   */\n  init() {\n    // not used\n  },\n  /**\n   * QUERY\n   *\n   * @param {String} q Query\n   * @param {Object} opts Additional options\n   */\n  async query(q, opts) {\n    const results = await WIKI.models.pages.query()\n      .column('pages.id', 'title', 'description', 'path', 'localeCode as locale')\n      .withGraphJoined('tags') // Adding page tags since they can be used to check resource access permissions\n      .modifyGraph('tags', builder => {\n        builder.select('tag')\n      })\n      .where(builder => {\n        builder.where('isPublished', true)\n        if (opts.locale) {\n          builder.andWhere('localeCode', opts.locale)\n        }\n        if (opts.path) {\n          builder.andWhere('path', 'like', `${opts.path}%`)\n        }\n        builder.andWhere(builderSub => {\n          if (WIKI.config.db.type === 'postgres') {\n            builderSub.where('title', 'ILIKE', `%${q}%`)\n            builderSub.orWhere('description', 'ILIKE', `%${q}%`)\n            builderSub.orWhere('path', 'ILIKE', `%${q.toLowerCase()}%`)\n          } else {\n            builderSub.where('title', 'LIKE', `%${q}%`)\n            builderSub.orWhere('description', 'LIKE', `%${q}%`)\n            builderSub.orWhere('path', 'LIKE', `%${q.toLowerCase()}%`)\n          }\n        })\n      })\n      .limit(WIKI.config.search.maxHits)\n    return {\n      results,\n      suggestions: [],\n      totalHits: results.length\n    }\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    // not used\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    // not used\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    // not used\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    // not used\n  },\n  /**\n   * REBUILD INDEX\n   */\n  async rebuild() {\n    // not used\n  }\n}\n"
  },
  {
    "path": "server/modules/search/elasticsearch/definition.yml",
    "content": "key: elasticsearch\ntitle: Elasticsearch\ndescription: Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/elasticsearch.svg\nwebsite: https://www.elastic.co/products/elasticsearch\nisAvailable: true\nprops:\n  apiVersion:\n    type: String\n    title: Elasticsearch Version\n    hint: Should match the version of the Elasticsearch nodes you are connecting to\n    order: 1\n    enum:\n      - '8.x'\n      - '7.x'\n      - '6.x'\n    default: '7.x'\n  hosts:\n    type: String\n    title: Host(s)\n    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)\n    order: 2\n  verifyTLSCertificate:\n    title: Verify TLS Certificate\n    type: Boolean\n    default: true\n    order: 3\n  tlsCertPath:\n    title: TLS Certificate Path\n    type: String\n    hint: Absolute path to the TLS certificate on the server.\n    order: 4\n  indexName:\n    type: String\n    title: Index Name\n    hint: The index name to use during creation\n    default: wiki\n    order: 5\n  analyzer:\n    type: String\n    title: Analyzer\n    hint: 'The token analyzer in elasticsearch'\n    default: simple\n    order: 6\n  sniffOnStart:\n    type: Boolean\n    title: Sniff on start\n    hint: 'Should Wiki.js attempt to detect the rest of the cluster on first connect? (Default: off)'\n    default: false\n    order: 7\n  sniffInterval:\n    type: Number\n    title: Sniff Interval\n    hint: '0 = disabled, Interval in seconds to check for updated list of nodes in cluster. (Default: 0)'\n    default: 0\n    order: 8\n"
  },
  {
    "path": "server/modules/search/elasticsearch/engine.js",
    "content": "const _ = require('lodash')\nconst fs = require('fs')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\n\n/* global WIKI */\n\nmodule.exports = {\n  async activate() {\n    // not used\n  },\n  async deactivate() {\n    // not used\n  },\n  /**\n   * INIT\n   */\n  async init() {\n    WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Initializing...`)\n    switch (this.config.apiVersion) {\n      case '8.x':\n        const { Client: Client8 } = require('elasticsearch8')\n        this.client = new Client8({\n          nodes: this.config.hosts.split(',').map(_.trim),\n          sniffOnStart: this.config.sniffOnStart,\n          sniffInterval: (this.config.sniffInterval > 0) ? this.config.sniffInterval : false,\n          tls: getTlsOptions(this.config),\n          name: 'wiki-js'\n        })\n        break\n      case '7.x':\n        const { Client: Client7 } = require('elasticsearch7')\n        this.client = new Client7({\n          nodes: this.config.hosts.split(',').map(_.trim),\n          sniffOnStart: this.config.sniffOnStart,\n          sniffInterval: (this.config.sniffInterval > 0) ? this.config.sniffInterval : false,\n          ssl: getTlsOptions(this.config),\n          name: 'wiki-js'\n        })\n        break\n      case '6.x':\n        const { Client: Client6 } = require('elasticsearch6')\n        this.client = new Client6({\n          nodes: this.config.hosts.split(',').map(_.trim),\n          sniffOnStart: this.config.sniffOnStart,\n          sniffInterval: (this.config.sniffInterval > 0) ? this.config.sniffInterval : false,\n          ssl: getTlsOptions(this.config),\n          name: 'wiki-js'\n        })\n        break\n      default:\n        throw new Error('Unsupported version of elasticsearch! Update your settings in the Administration Area.')\n    }\n\n    // -> Create Search Index\n    await this.createIndex()\n\n    WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Initialization completed.`)\n  },\n  /**\n   * Create Index\n   */\n  async createIndex() {\n    try {\n      const indexExists = await this.client.indices.exists({ index: this.config.indexName })\n      // Elasticsearch 6.x / 7.x\n      if (this.config.apiVersion !== '8.x' && !indexExists.body) {\n        WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Creating index...`)\n        try {\n          const idxBody = {\n            properties: {\n              suggest: { type: 'completion' },\n              title: { type: 'text', boost: 10.0 },\n              description: { type: 'text', boost: 3.0 },\n              content: { type: 'text', boost: 1.0 },\n              locale: { type: 'keyword' },\n              path: { type: 'text' },\n              tags: { type: 'text', boost: 8.0 }\n            }\n          }\n\n          await this.client.indices.create({\n            index: this.config.indexName,\n            body: {\n              mappings: (this.config.apiVersion === '6.x') ? {\n                _doc: idxBody\n              } : idxBody,\n              settings: {\n                analysis: {\n                  analyzer: {\n                    default: {\n                      type: this.config.analyzer\n                    }\n                  }\n                }\n              }\n            }\n          })\n        } catch (err) {\n          WIKI.logger.error(`(SEARCH/ELASTICSEARCH) Create Index Error: `, _.get(err, 'meta.body.error', err))\n        }\n      // Elasticsearch 8.x\n      } else if (this.config.apiVersion === '8.x' && !indexExists) {\n        WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Creating index...`)\n        try {\n          // 8.x Doesn't support boost in mappings, so we will need to boost at query time.\n          const idxBody = {\n            properties: {\n              suggest: { type: 'completion' },\n              title: { type: 'text' },\n              description: { type: 'text' },\n              content: { type: 'text' },\n              locale: { type: 'keyword' },\n              path: { type: 'text' },\n              tags: { type: 'text' }\n            }\n          }\n\n          await this.client.indices.create({\n            index: this.config.indexName,\n            body: {\n              mappings: idxBody,\n              settings: {\n                analysis: {\n                  analyzer: {\n                    default: {\n                      type: this.config.analyzer\n                    }\n                  }\n                }\n              }\n            }\n          })\n        } catch (err) {\n          WIKI.logger.error(`(SEARCH/ELASTICSEARCH) Create Index Error: `, _.get(err, 'meta.body.error', err))\n        }\n      }\n    } catch (err) {\n      WIKI.logger.error(`(SEARCH/ELASTICSEARCH) Index Check Error: `, _.get(err, 'meta.body.error', err))\n    }\n  },\n  /**\n   * QUERY\n   *\n   * @param {String} q Query\n   * @param {Object} opts Additional options\n   */\n  async query(q, opts) {\n    try {\n      const results = await this.client.search({\n        index: this.config.indexName,\n        body: {\n          query: {\n            simple_query_string: {\n              query: `*${q}*`,\n              fields: ['title^20', 'description^3', 'tags^8', 'content^1'],\n              default_operator: 'and',\n              analyze_wildcard: true\n            }\n          },\n          from: 0,\n          size: 50,\n          _source: ['title', 'description', 'path', 'locale'],\n          suggest: {\n            suggestions: {\n              text: q,\n              completion: {\n                field: 'suggest',\n                size: 5,\n                skip_duplicates: true,\n                fuzzy: true\n              }\n            }\n          }\n        }\n      })\n      return {\n        results: _.get(results, this.config.apiVersion === '8.x' ? 'hits.hits' : 'body.hits.hits', []).map(r => ({\n          id: r._id,\n          locale: r._source.locale,\n          path: r._source.path,\n          title: r._source.title,\n          description: r._source.description\n        })),\n        suggestions: _.reject(_.get(results, 'suggest.suggestions', []).map(s => _.get(s, 'options[0].text', false)), s => !s),\n        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))\n      }\n    } catch (err) {\n      WIKI.logger.warn('Search Engine Error: ', _.get(err, 'meta.body.error', err))\n    }\n  },\n\n  /**\n   * Build tags field\n   * @param id\n   * @returns {Promise<*|*[]>}\n   */\n  async buildTags(id) {\n    const tags = await WIKI.models.pages.query().findById(id).select('*').withGraphJoined('tags')\n    return (tags.tags && tags.tags.length > 0) ? tags.tags.map(function (tag) {\n      return tag.title\n    }) : []\n  },\n  /**\n   * Build suggest field\n   */\n  buildSuggest(page) {\n    return _.reject(_.uniq(_.concat(\n      page.title.split(' ').map(s => ({\n        input: s,\n        weight: 10\n      })),\n      page.description.split(' ').map(s => ({\n        input: s,\n        weight: 3\n      })),\n      page.safeContent.split(' ').map(s => ({\n        input: s,\n        weight: 1\n      }))\n    )), ['input', ''])\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    await this.client.index({\n      index: this.config.indexName,\n      ...(this.config.apiVersion !== '8.x' && { type: '_doc' }),\n      id: page.hash,\n      body: {\n        suggest: this.buildSuggest(page),\n        locale: page.localeCode,\n        path: page.path,\n        title: page.title,\n        description: page.description,\n        content: page.safeContent,\n        tags: await this.buildTags(page.id)\n      },\n      refresh: true\n    })\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    await this.client.index({\n      index: this.config.indexName,\n      ...(this.config.apiVersion !== '8.x' && { type: '_doc' }),\n      id: page.hash,\n      body: {\n        suggest: this.buildSuggest(page),\n        locale: page.localeCode,\n        path: page.path,\n        title: page.title,\n        description: page.description,\n        content: page.safeContent,\n        tags: await this.buildTags(page.id)\n      },\n      refresh: true\n    })\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    await this.client.delete({\n      index: this.config.indexName,\n      ...(this.config.apiVersion !== '8.x' && { type: '_doc' }),\n      id: page.hash,\n      refresh: true\n    })\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    await this.client.delete({\n      index: this.config.indexName,\n      ...(this.config.apiVersion !== '8.x' && { type: '_doc' }),\n      id: page.hash,\n      refresh: true\n    })\n    await this.client.index({\n      index: this.config.indexName,\n      ...(this.config.apiVersion !== '8.x' && { type: '_doc' }),\n      id: page.destinationHash,\n      body: {\n        suggest: this.buildSuggest(page),\n        locale: page.destinationLocaleCode,\n        path: page.destinationPath,\n        title: page.title,\n        description: page.description,\n        content: page.safeContent,\n        tags: await this.buildTags(page.id)\n      },\n      refresh: true\n    })\n  },\n  /**\n   * REBUILD INDEX\n   */\n  async rebuild() {\n    WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Rebuilding Index...`)\n    await this.client.indices.delete({ index: this.config.indexName })\n    await this.createIndex()\n\n    const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB\n    const MAX_INDEXING_COUNT = 1000\n    const COMMA_BYTES = Buffer.from(',').byteLength\n\n    let chunks = []\n    let bytes = 0\n\n    const processDocument = async (cb, doc) => {\n      try {\n        if (doc) {\n          const docBytes = Buffer.from(JSON.stringify(doc)).byteLength\n\n          doc['tags'] = await this.buildTags(doc.realId)\n          // -> Current batch exceeds size limit, flush\n          if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) {\n            await flushBuffer()\n          }\n\n          if (chunks.length > 0) {\n            bytes += COMMA_BYTES\n          }\n          bytes += docBytes\n          chunks.push(doc)\n\n          // -> Current batch exceeds count limit, flush\n          if (chunks.length >= MAX_INDEXING_COUNT) {\n            await flushBuffer()\n          }\n        } else {\n          // -> End of stream, flush\n          await flushBuffer()\n        }\n        cb()\n      } catch (err) {\n        cb(err)\n      }\n    }\n\n    const flushBuffer = async () => {\n      WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Sending batch of ${chunks.length}...`)\n      try {\n        await this.client.bulk({\n          index: this.config.indexName,\n          body: _.reduce(chunks, (result, doc) => {\n            result.push({\n              index: {\n                _index: this.config.indexName,\n                _id: doc.id,\n                ...(this.config.apiVersion !== '8.x' && { _type: '_doc' })\n              }\n            })\n            doc.safeContent = WIKI.models.pages.cleanHTML(doc.render)\n            result.push({\n              suggest: this.buildSuggest(doc),\n              tags: doc.tags,\n              locale: doc.locale,\n              path: doc.path,\n              title: doc.title,\n              description: doc.description,\n              content: doc.safeContent\n            })\n            return result\n          }, []),\n          refresh: true\n        })\n      } catch (err) {\n        WIKI.logger.warn('(SEARCH/ELASTICSEARCH) Failed to send batch to elasticsearch: ', err)\n      }\n      chunks.length = 0\n      bytes = 0\n    }\n\n    // Added real id in order to fetch page tags from the query\n    await pipeline(\n      WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render', { realId: 'id' }).select().from('pages').where({\n        isPublished: true,\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (chunk, enc, cb) => processDocument(cb, chunk),\n        flush: async (cb) => processDocument(cb)\n      })\n    )\n    WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Index rebuilt successfully.`)\n  }\n}\n\nfunction getTlsOptions(conf) {\n  if (!conf.tlsCertPath) {\n    return {\n      rejectUnauthorized: conf.verifyTLSCertificate\n    }\n  }\n\n  const caList = []\n  if (conf.verifyTLSCertificate) {\n    caList.push(fs.readFileSync(conf.tlsCertPath))\n  }\n\n  return {\n    rejectUnauthorized: conf.verifyTLSCertificate,\n    ca: caList\n  }\n}\n"
  },
  {
    "path": "server/modules/search/manticore/definition.yml",
    "content": "key: manticore\ntitle: Manticore Search\ndescription: High performance full-text search engine with SQL and JSON support.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/manticore.svg\nwebsite: https://manticoresearch.com/\nisAvailable: false\nprops: {}\n"
  },
  {
    "path": "server/modules/search/manticore/engine.js",
    "content": "module.exports = {\n  activate() {\n\n  },\n  deactivate() {\n\n  },\n  query() {\n\n  },\n  created() {\n\n  },\n  updated() {\n\n  },\n  deleted() {\n\n  },\n  renamed() {\n\n  },\n  rebuild() {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/search/postgres/definition.yml",
    "content": "key: postgres\ntitle: Database - PostgreSQL\ndescription: Advanced PostgreSQL-based search engine.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/postgresql.svg\nwebsite: https://www.requarks.io/\nisAvailable: true\nprops:\n  dictLanguage:\n    type: String\n    title: Dictionary Language\n    hint: Language to use when creating and querying text search vectors.\n    default: english\n    enum:\n      - simple\n      - danish\n      - dutch\n      - english\n      - finnish\n      - french\n      - german\n      - hungarian\n      - italian\n      - norwegian\n      - portuguese\n      - romanian\n      - russian\n      - spanish\n      - swedish\n      - turkish\n    order: 1\n"
  },
  {
    "path": "server/modules/search/postgres/engine.js",
    "content": "const tsquery = require('pg-tsquery')()\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\n\n/* global WIKI */\n\nmodule.exports = {\n  async activate() {\n    if (WIKI.config.db.type !== 'postgres') {\n      throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!')\n    }\n  },\n  async deactivate() {\n    WIKI.logger.info(`(SEARCH/POSTGRES) Dropping index tables...`)\n    await WIKI.models.knex.schema.dropTable('pagesWords')\n    await WIKI.models.knex.schema.dropTable('pagesVector')\n    WIKI.logger.info(`(SEARCH/POSTGRES) Index tables have been dropped.`)\n  },\n  /**\n   * INIT\n   */\n  async init() {\n    WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`)\n\n    // -> Ensure pg_trgm extension is available (required for similarity search)\n    await WIKI.models.knex.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm')\n\n    // -> Create Search Index\n    const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')\n    if (!indexExists) {\n      WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`)\n      await WIKI.models.knex.schema.createTable('pagesVector', table => {\n        table.increments()\n        table.string('path')\n        table.string('locale')\n        table.string('title')\n        table.string('description')\n        table.specificType('tokens', 'TSVECTOR')\n        table.text('content')\n      })\n    }\n    // -> Create Words Index\n    const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords')\n    if (!wordsExists) {\n      WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`)\n      await WIKI.models.knex.raw(`\n        CREATE TABLE \"pagesWords\" AS SELECT word FROM ts_stat(\n          'SELECT to_tsvector(''simple'', \"title\") || to_tsvector(''simple'', \"description\") || to_tsvector(''simple'', \"content\") FROM \"pagesVector\"'\n        )`)\n      await WIKI.models.knex.raw(`CREATE INDEX \"pageWords_idx\" ON \"pagesWords\" USING GIN (word gin_trgm_ops)`)\n    }\n\n    WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`)\n  },\n  /**\n   * QUERY\n   *\n   * @param {String} q Query\n   * @param {Object} opts Additional options\n   */\n  async query(q, opts) {\n    try {\n      let suggestions = []\n      let qry = `\n        SELECT id, path, locale, title, description\n        FROM \"pagesVector\", to_tsquery(?,?) query\n        WHERE (query @@ \"tokens\" OR path ILIKE ?)\n      `\n      let qryEnd = `ORDER BY ts_rank(tokens, query) DESC`\n      let qryParams = [this.config.dictLanguage, tsquery(q), `%${q.toLowerCase()}%`]\n\n      if (opts.locale) {\n        qry = `${qry} AND locale = ?`\n        qryParams.push(opts.locale)\n      }\n      if (opts.path) {\n        qry = `${qry} AND path ILIKE ?`\n        qryParams.push(`%${opts.path}`)\n      }\n      const results = await WIKI.models.knex.raw(`\n        ${qry}\n        ${qryEnd}\n      `, qryParams)\n      if (results.rows.length < 5) {\n        try {\n          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])\n          suggestions = suggestResults.rows.map(r => r.word)\n        } catch (err) {\n          WIKI.logger.warn(`Search Engine Suggestion Error (pg_trgm extension may be missing): ${err.message}`)\n        }\n      }\n      return {\n        results: results.rows,\n        suggestions,\n        totalHits: results.rows.length\n      }\n    } catch (err) {\n      WIKI.logger.warn('Search Engine Error:')\n      WIKI.logger.warn(err)\n    }\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    await WIKI.models.knex.raw(`\n      INSERT INTO \"pagesVector\" (path, locale, title, description, \"tokens\") VALUES (\n        ?, ?, ?, ?, (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C'))\n      )\n    `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.safeContent])\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    await WIKI.models.knex.raw(`\n      UPDATE \"pagesVector\" SET\n        title = ?,\n        description = ?,\n        tokens = (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') ||\n        setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') ||\n        setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C'))\n      WHERE path = ? AND locale = ?\n    `, [page.title, page.description, page.title, page.description, page.safeContent, page.path, page.localeCode])\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    await WIKI.models.knex('pagesVector').where({\n      locale: page.localeCode,\n      path: page.path\n    }).del().limit(1)\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    await WIKI.models.knex('pagesVector').where({\n      locale: page.localeCode,\n      path: page.path\n    }).update({\n      locale: page.destinationLocaleCode,\n      path: page.destinationPath\n    })\n  },\n  /**\n   * REBUILD INDEX\n   */\n  async rebuild() {\n    WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`)\n    await WIKI.models.knex('pagesVector').truncate()\n    await WIKI.models.knex('pagesWords').truncate()\n\n    await pipeline(\n      WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'render').select().from('pages').where({\n        isPublished: true,\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (page, enc, cb) => {\n          const content = WIKI.models.pages.cleanHTML(page.render)\n          await WIKI.models.knex.raw(`\n            INSERT INTO \"pagesVector\" (path, locale, title, description, \"tokens\", content) VALUES (\n              ?, ?, ?, ?, (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C')), ?\n            )\n          `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, content, content])\n          cb()\n        }\n      })\n    )\n\n    await WIKI.models.knex.raw(`\n      INSERT INTO \"pagesWords\" (word)\n        SELECT word FROM ts_stat(\n          'SELECT to_tsvector(''simple'', \"title\") || to_tsvector(''simple'', \"description\") || to_tsvector(''simple'', \"content\") FROM \"pagesVector\"'\n        )\n      `)\n\n    WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`)\n  }\n}\n"
  },
  {
    "path": "server/modules/search/solr/definition.yml",
    "content": "key: solr\ntitle: Solr\ndescription: Solr is the popular, blazing-fast, open source enterprise search platform built on Apache Lucene.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/solr.svg\nwebsite: http://lucene.apache.org/solr/\nisAvailable: false\nprops:\n  host:\n    type: String\n    title: Host\n    hint: Host of the Solr server (e.g. 12.34.56.78 or solr.example.com)\n    default: solr\n    order: 1\n  port:\n    type: Number\n    title: Port\n    hint: Port of the Solr server\n    default: 8983\n    order: 2\n  core:\n    type: String\n    title: Core\n    hint: Core name (e.g. wiki)\n    default: wiki\n    order: 3\n  protocol:\n    type: String\n    title: Protocol\n    hint: Request protocol\n    default: http\n    enum:\n      - http\n      - https\n    order: 4\n"
  },
  {
    "path": "server/modules/search/solr/engine.js",
    "content": "module.exports = {\n  activate() {\n\n  },\n  deactivate() {\n\n  },\n  query() {\n\n  },\n  created() {\n\n  },\n  updated() {\n\n  },\n  deleted() {\n\n  },\n  renamed() {\n\n  },\n  rebuild() {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/search/sphinx/definition.yml",
    "content": "key: sphinx\ntitle: Sphinx\ndescription: Sphinx is an open source full text search server, designed from the ground up with performance, relevance and integration simplicity in mind.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/sphinx.svg\nwebsite: http://sphinxsearch.com/\nisAvailable: false\nprops: {}\n"
  },
  {
    "path": "server/modules/search/sphinx/engine.js",
    "content": "module.exports = {\n  activate() {\n\n  },\n  deactivate() {\n\n  },\n  query() {\n\n  },\n  created() {\n\n  },\n  updated() {\n\n  },\n  deleted() {\n\n  },\n  renamed() {\n\n  },\n  rebuild() {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/azure/definition.yml",
    "content": "key: azure\ntitle: Azure Blob Storage\ndescription: Azure Blob Storage by Microsoft provides massively scalable object storage for unstructured data.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/azure.svg\nwebsite: https://azure.microsoft.com/services/storage/blobs/\nisAvailable: true\nsupportedModes:\n  - push\ndefaultMode: push\nschedule: false\nprops:\n  accountName:\n    type: String\n    title: Account Name\n    default: ''\n    hint: Your unique account name.\n    order: 1\n  accountKey:\n    type: String\n    title: Account Access Key\n    default: ''\n    hint: Either key 1 or key 2.\n    sensitive: true\n    order: 2\n  containerName:\n    type: String\n    title: Container Name\n    default: 'wiki'\n    hint: Will automatically be created if it doesn't exist yet.\n    order: 3\n  storageTier:\n    type: String\n    title: Storage Tier\n    hint: Represents the access tier on a blob. Use Cool for lower storage costs but at higher retrieval costs.\n    order: 4\n    default: 'Cool'\n    enum:\n        - 'Hot'\n        - 'Cool'\nactions:\n  - handler: exportAll\n    label: Export All\n    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.\n"
  },
  {
    "path": "server/modules/storage/azure/storage.js",
    "content": "const { BlobServiceClient, StorageSharedKeyCredential } = require('@azure/storage-blob')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\nconst pageHelper = require('../../../helpers/page.js')\nconst _ = require('lodash')\n\n/* global WIKI */\n\nconst getFilePath = (page, pathKey) => {\n  const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}`\n  const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode\n  return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName\n}\n\nmodule.exports = {\n  async activated() {\n\n  },\n  async deactivated() {\n\n  },\n  async init() {\n    WIKI.logger.info(`(STORAGE/AZURE) Initializing...`)\n    const { accountName, accountKey, containerName } = this.config\n    this.client = new BlobServiceClient(\n      `https://${accountName}.blob.core.windows.net`,\n      new StorageSharedKeyCredential(accountName, accountKey)\n    )\n    this.container = this.client.getContainerClient(containerName)\n    try {\n      await this.container.create()\n    } catch (err) {\n      if (err.statusCode !== 409) {\n        WIKI.logger.warn(err)\n        throw err\n      }\n    }\n    WIKI.logger.info(`(STORAGE/AZURE) Initialization completed.`)\n  },\n  async created (page) {\n    WIKI.logger.info(`(STORAGE/AZURE) Creating file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    const pageContent = page.injectMetadata()\n    const blockBlobClient = this.container.getBlockBlobClient(filePath)\n    await blockBlobClient.upload(pageContent, pageContent.length, { tier: this.config.storageTier })\n  },\n  async updated (page) {\n    WIKI.logger.info(`(STORAGE/AZURE) Updating file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    const pageContent = page.injectMetadata()\n    const blockBlobClient = this.container.getBlockBlobClient(filePath)\n    await blockBlobClient.upload(pageContent, pageContent.length, { tier: this.config.storageTier })\n  },\n  async deleted (page) {\n    WIKI.logger.info(`(STORAGE/AZURE) Deleting file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    const blockBlobClient = this.container.getBlockBlobClient(filePath)\n    await blockBlobClient.delete({\n      deleteSnapshots: 'include'\n    })\n  },\n  async renamed(page) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file ${page.path} to ${page.destinationPath}...`)\n    let sourceFilePath = getFilePath(page, 'path')\n    let destinationFilePath = getFilePath(page, 'destinationPath')\n    if (WIKI.config.lang.namespacing) {\n      if (WIKI.config.lang.code !== page.localeCode) {\n        sourceFilePath = `${page.localeCode}/${sourceFilePath}`\n      }\n      if (WIKI.config.lang.code !== page.destinationLocaleCode) {\n        destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`\n      }\n    }\n    const sourceBlockBlobClient = this.container.getBlockBlobClient(sourceFilePath)\n    const destBlockBlobClient = this.container.getBlockBlobClient(destinationFilePath)\n    await destBlockBlobClient.syncCopyFromURL(sourceBlockBlobClient.url)\n    await sourceBlockBlobClient.delete({\n      deleteSnapshots: 'include'\n    })\n  },\n  /**\n   * ASSET UPLOAD\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetUploaded (asset) {\n    WIKI.logger.info(`(STORAGE/AZURE) Creating new file ${asset.path}...`)\n    const blockBlobClient = this.container.getBlockBlobClient(asset.path)\n    await blockBlobClient.upload(asset.data, asset.data.length, { tier: this.config.storageTier })\n  },\n  /**\n   * ASSET DELETE\n   *\n   * @param {Object} asset Asset to delete\n   */\n  async assetDeleted (asset) {\n    WIKI.logger.info(`(STORAGE/AZURE) Deleting file ${asset.path}...`)\n    const blockBlobClient = this.container.getBlockBlobClient(asset.path)\n    await blockBlobClient.delete({\n      deleteSnapshots: 'include'\n    })\n  },\n  /**\n   * ASSET RENAME\n   *\n   * @param {Object} asset Asset to rename\n   */\n  async assetRenamed (asset) {\n    WIKI.logger.info(`(STORAGE/AZURE) Renaming file from ${asset.path} to ${asset.destinationPath}...`)\n    const sourceBlockBlobClient = this.container.getBlockBlobClient(asset.path)\n    const destBlockBlobClient = this.container.getBlockBlobClient(asset.destinationPath)\n    await destBlockBlobClient.syncCopyFromURL(sourceBlockBlobClient.url)\n    await sourceBlockBlobClient.delete({\n      deleteSnapshots: 'include'\n    })\n  },\n  async getLocalLocation () {\n\n  },\n  /**\n   * HANDLERS\n   */\n  async exportAll() {\n    WIKI.logger.info(`(STORAGE/AZURE) Exporting all content to Azure Blob Storage...`)\n\n    // -> Pages\n    await pipeline(\n      WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt').select().from('pages').where({\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (page, enc, cb) => {\n          const filePath = getFilePath(page, 'path')\n          WIKI.logger.info(`(STORAGE/AZURE) Adding page ${filePath}...`)\n          const pageContent = pageHelper.injectPageMetadata(page)\n          const blockBlobClient = this.container.getBlockBlobClient(filePath)\n          await blockBlobClient.upload(pageContent, pageContent.length, { tier: this.config.storageTier })\n          cb()\n        }\n      })\n    )\n\n    // -> Assets\n    const assetFolders = await WIKI.models.assetFolders.getAllPaths()\n\n    await pipeline(\n      WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (asset, enc, cb) => {\n          const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename\n          WIKI.logger.info(`(STORAGE/AZURE) Adding asset ${filename}...`)\n          const blockBlobClient = this.container.getBlockBlobClient(filename)\n          await blockBlobClient.upload(asset.data, asset.data.length, { tier: this.config.storageTier })\n          cb()\n        }\n      })\n    )\n\n    WIKI.logger.info('(STORAGE/AZURE) All content has been pushed to Azure Blob Storage.')\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/box/definition.yml",
    "content": "key: box\ntitle: Box\ndescription: Box is a cloud content management and file sharing service for businesses.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/box.svg\nwebsite: https://www.box.com/platform\nprops:\n  clientId: String\n  clientSecret: String\n  rootFolder: String\n"
  },
  {
    "path": "server/modules/storage/box/storage.js",
    "content": "module.exports = {\n  async activated() {\n\n  },\n  async deactivated() {\n\n  },\n  async init() {\n\n  },\n  async created() {\n\n  },\n  async updated() {\n\n  },\n  async deleted() {\n\n  },\n  async renamed() {\n\n  },\n  async getLocalLocation () {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/digitalocean/definition.yml",
    "content": "key: digitalocean\ntitle: DigitalOcean Spaces\ndescription: DigitalOcean provides developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces) and more.\nauthor: andrewsim\nlogo: https://static.requarks.io/logo/digitalocean.svg\nwebsite: https://www.digitalocean.com/products/spaces/\nisAvailable: true\nsupportedModes:\n  - push\ndefaultMode: push\nschedule: false\nprops:\n  endpoint:\n    type: String\n    title: Endpoint\n    hint: The DigitalOcean spaces endpoint that has the form ${REGION}.digitaloceanspaces.com\n    default: nyc3.digitaloceanspaces.com\n    enum:\n      - ams3.digitaloceanspaces.com\n      - fra1.digitaloceanspaces.com\n      - nyc3.digitaloceanspaces.com\n      - sfo2.digitaloceanspaces.com\n      - sfo3.digitaloceanspaces.com\n      - sgp1.digitaloceanspaces.com\n      - tor1.digitaloceanspaces.com\n    order: 1\n  bucket:\n    type: String\n    title: Space Unique Name\n    hint: The unique space name to create (e.g. wiki-johndoe)\n    order: 2\n  accessKeyId:\n    type: String\n    title: Access Key ID\n    hint: The Access Key (Generated in API > Tokens/Keys > Spaces access keys).\n    order: 3\n  secretAccessKey :\n    type: String\n    title: Access Key Secret\n    hint: The Access Key Secret for the Access Key ID you created above.\n    sensitive: true\n    order: 4\nactions:\n  - handler: exportAll\n    label: Export All\n    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.\n\n"
  },
  {
    "path": "server/modules/storage/digitalocean/storage.js",
    "content": "const S3CompatibleStorage = require('../s3/common')\n\nmodule.exports = new S3CompatibleStorage('Digitalocean')\n"
  },
  {
    "path": "server/modules/storage/disk/common.js",
    "content": "const fs = require('fs-extra')\nconst path = require('path')\nconst { pipeline } = require('stream/promises')\nconst { Transform } = require('stream')\nconst klaw = require('klaw')\nconst mime = require('mime-types').lookup\nconst _ = require('lodash')\n\nconst pageHelper = require('../../../helpers/page.js')\n\n/* global WIKI */\n\nmodule.exports = {\n  assetFolders: null,\n  async importFromDisk ({ fullPath, moduleName }) {\n    const rootUser = await WIKI.models.users.getRootUser()\n\n    await pipeline(\n      klaw(fullPath, {\n        filter: (f) => {\n          return !_.includes(f, '.git')\n        }\n      }),\n      new Transform({\n        objectMode: true,\n        transform: async (file, enc, cb) => {\n          const relPath = file.path.substr(fullPath.length + 1)\n          if (file.stats.size < 1) {\n            // Skip directories and zero-byte files\n            return cb()\n          } else if (relPath && relPath.length > 3) {\n            WIKI.logger.info(`(STORAGE/${moduleName}) Processing ${relPath}...`)\n            const contentType = pageHelper.getContentType(relPath)\n            if (contentType) {\n              // -> Page\n\n              try {\n                await this.processPage({\n                  user: rootUser,\n                  relPath: relPath,\n                  fullPath: fullPath,\n                  contentType: contentType,\n                  moduleName: moduleName\n                })\n              } catch (err) {\n                WIKI.logger.warn(`(STORAGE/${moduleName}) Failed to process page ${relPath}`)\n                WIKI.logger.warn(err)\n              }\n            } else {\n              // -> Asset\n\n              try {\n                await this.processAsset({\n                  user: rootUser,\n                  relPath: relPath,\n                  file: file,\n                  contentType: contentType,\n                  moduleName: moduleName\n                })\n              } catch (err) {\n                WIKI.logger.warn(`(STORAGE/${moduleName}) Failed to process asset ${relPath}`)\n                WIKI.logger.warn(err)\n              }\n            }\n          }\n          cb()\n        }\n      })\n    )\n    this.clearFolderCache()\n  },\n\n  async processPage ({ user, fullPath, relPath, contentType, moduleName }) {\n    const normalizedRelPath = relPath.replace(/\\\\/g, '/')\n    const contentPath = pageHelper.getPagePath(normalizedRelPath)\n    const itemContents = await fs.readFile(path.join(fullPath, relPath), 'utf8')\n    const pageData = WIKI.models.pages.parseMetadata(itemContents, contentType)\n    const currentPage = await WIKI.models.pages.getPageFromDb({\n      path: contentPath.path,\n      locale: contentPath.locale\n    })\n    const newTags = !_.isNil(pageData.tags) ? _.get(pageData, 'tags', '').split(', ') : false\n    if (currentPage) {\n      // Already in the DB, can mark as modified\n      WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as modified: ${normalizedRelPath}`)\n      await WIKI.models.pages.updatePage({\n        id: currentPage.id,\n        title: _.get(pageData, 'title', currentPage.title),\n        description: _.get(pageData, 'description', currentPage.description) || '',\n        tags: newTags || currentPage.tags.map(t => t.tag),\n        isPublished: _.get(pageData, 'isPublished', currentPage.isPublished),\n        isPrivate: false,\n        content: pageData.content,\n        user: user,\n        skipStorage: true\n      })\n    } else {\n      // Not in the DB, can mark as new\n      WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as new: ${normalizedRelPath}`)\n      const pageEditor = await WIKI.models.editors.getDefaultEditor(contentType)\n      await WIKI.models.pages.createPage({\n        path: contentPath.path,\n        locale: contentPath.locale,\n        title: _.get(pageData, 'title', _.last(contentPath.path.split('/'))),\n        description: _.get(pageData, 'description', '') || '',\n        tags: newTags || [],\n        isPublished: _.get(pageData, 'isPublished', true),\n        isPrivate: false,\n        content: pageData.content,\n        user: user,\n        editor: pageEditor,\n        skipStorage: true\n      })\n    }\n  },\n\n  async processAsset ({ user, relPath, file, moduleName }) {\n    WIKI.logger.info(`(STORAGE/${moduleName}) Asset marked for import: ${relPath}`)\n\n    // -> Get all folder paths\n    if (!this.assetFolders) {\n      this.assetFolders = await WIKI.models.assetFolders.getAllPaths()\n    }\n\n    // -> Find existing folder\n    const filePathInfo = path.parse(file.path)\n    const folderPath = path.dirname(relPath).replace(/\\\\/g, '/')\n    let folderId = _.toInteger(_.findKey(this.assetFolders, fld => { return fld === folderPath })) || null\n\n    // -> Create missing folder structure\n    if (!folderId && folderPath !== '.') {\n      const folderParts = folderPath.split('/')\n      let currentFolderPath = []\n      let currentFolderParentId = null\n      for (const folderPart of folderParts) {\n        currentFolderPath.push(folderPart)\n        const existingFolderId = _.findKey(this.assetFolders, fld => { return fld === currentFolderPath.join('/') })\n        if (!existingFolderId) {\n          const newFolderObj = await WIKI.models.assetFolders.query().insert({\n            slug: folderPart,\n            name: folderPart,\n            parentId: currentFolderParentId\n          })\n          _.set(this.assetFolders, newFolderObj.id, currentFolderPath.join('/'))\n          currentFolderParentId = newFolderObj.id\n        } else {\n          currentFolderParentId = _.toInteger(existingFolderId)\n        }\n      }\n      folderId = currentFolderParentId\n    }\n\n    // -> Import asset\n    await WIKI.models.assets.upload({\n      mode: 'import',\n      originalname: filePathInfo.base,\n      ext: filePathInfo.ext,\n      mimetype: mime(filePathInfo.base) || 'application/octet-stream',\n      size: file.stats.size,\n      folderId: folderId,\n      path: file.path,\n      assetPath: relPath,\n      user: user,\n      skipStorage: true\n    })\n  },\n\n  clearFolderCache () {\n    this.assetFolders = null\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/disk/definition.yml",
    "content": "key: disk\ntitle: Local File System\ndescription: Local storage on disk or network shares.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/local-fs.svg\nwebsite: https://wiki.js.org\nisAvailable: true\nsupportedModes:\n  - push\ndefaultMode: push\nschedule: false\ninternalSchedule: P1D\nprops:\n  path:\n    type: String\n    title: Path\n    hint: Absolute path without a trailing slash (e.g. /home/wiki/backup, C:\\wiki\\backup)\n    order: 1\n  createDailyBackups:\n    type: Boolean\n    default: false\n    title: Create Daily Backups\n    hint: A tar.gz archive containing all content will be created daily in subfolder named _daily. Archives are kept for a month.\n    order: 2\nactions:\n  - handler: dump\n    label: Dump all content to disk\n    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.\n  - handler: backup\n    label: Create Backup\n    hint: Will create a manual backup archive at this point in time, in a subfolder named _manual, from the contents currently on disk.\n  - handler: importAll\n    label: Import Everything\n    hint: Will import all content currently in the local disk folder.\n"
  },
  {
    "path": "server/modules/storage/disk/storage.js",
    "content": "const fs = require('fs-extra')\nconst path = require('path')\nconst tar = require('tar-fs')\nconst zlib = require('zlib')\nconst _ = require('lodash')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\nconst moment = require('moment')\n\nconst pageHelper = require('../../../helpers/page')\nconst commonDisk = require('./common')\n\n/* global WIKI */\n\nmodule.exports = {\n  async activated() {\n    // not used\n  },\n  async deactivated() {\n    // not used\n  },\n  async init() {\n    WIKI.logger.info('(STORAGE/DISK) Initializing...')\n    await fs.ensureDir(this.config.path)\n    WIKI.logger.info('(STORAGE/DISK) Initialization completed.')\n  },\n  async sync({ manual } = { manual: false }) {\n    if (this.config.createDailyBackups || manual) {\n      const dirPath = path.join(this.config.path, manual ? '_manual' : '_daily')\n      await fs.ensureDir(dirPath)\n\n      const dateFilename = moment().format(manual ? 'YYYYMMDD-HHmmss' : 'DD')\n\n      WIKI.logger.info(`(STORAGE/DISK) Creating backup archive...`)\n      await pipeline(\n        tar.pack(this.config.path, {\n          ignore: (filePath) => {\n            return filePath.indexOf('_daily') >= 0 || filePath.indexOf('_manual') >= 0\n          }\n        }),\n        zlib.createGzip(),\n        fs.createWriteStream(path.join(dirPath, `wiki-${dateFilename}.tar.gz`))\n      )\n      WIKI.logger.info('(STORAGE/DISK) Backup archive created successfully.')\n    }\n  },\n  async created(page) {\n    WIKI.logger.info(`(STORAGE/DISK) Creating file [${page.localeCode}] ${page.path}...`)\n    let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    if (WIKI.config.lang.code !== page.localeCode) {\n      fileName = `${page.localeCode}/${fileName}`\n    }\n    const filePath = path.join(this.config.path, fileName)\n    await fs.outputFile(filePath, page.injectMetadata(), 'utf8')\n  },\n  async updated(page) {\n    WIKI.logger.info(`(STORAGE/DISK) Updating file [${page.localeCode}] ${page.path}...`)\n    let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    if (WIKI.config.lang.code !== page.localeCode) {\n      fileName = `${page.localeCode}/${fileName}`\n    }\n    const filePath = path.join(this.config.path, fileName)\n    await fs.outputFile(filePath, page.injectMetadata(), 'utf8')\n  },\n  async deleted(page) {\n    WIKI.logger.info(`(STORAGE/DISK) Deleting file [${page.localeCode}] ${page.path}...`)\n    let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    if (WIKI.config.lang.code !== page.localeCode) {\n      fileName = `${page.localeCode}/${fileName}`\n    }\n    const filePath = path.join(this.config.path, fileName)\n    await fs.unlink(filePath)\n  },\n  async renamed(page) {\n    WIKI.logger.info(`(STORAGE/DISK) Renaming file [${page.localeCode}] ${page.path} to [${page.destinationLocaleCode}] ${page.destinationPath}...`)\n\n    let sourceFilePath = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    let destinationFilePath = `${page.destinationPath}.${pageHelper.getFileExtension(page.contentType)}`\n\n    if (WIKI.config.lang.namespacing) {\n      if (WIKI.config.lang.code !== page.localeCode) {\n        sourceFilePath = `${page.localeCode}/${sourceFilePath}`\n      }\n      if (WIKI.config.lang.code !== page.destinationLocaleCode) {\n        destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`\n      }\n    }\n\n    await fs.move(path.join(this.config.path, sourceFilePath), path.join(this.config.path, destinationFilePath), { overwrite: true })\n  },\n  /**\n   * ASSET UPLOAD\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetUploaded (asset) {\n    WIKI.logger.info(`(STORAGE/DISK) Creating new file ${asset.path}...`)\n    await fs.outputFile(path.join(this.config.path, asset.path), asset.data)\n  },\n  /**\n   * ASSET DELETE\n   *\n   * @param {Object} asset Asset to delete\n   */\n  async assetDeleted (asset) {\n    WIKI.logger.info(`(STORAGE/DISK) Deleting file ${asset.path}...`)\n    await fs.remove(path.join(this.config.path, asset.path))\n  },\n  /**\n   * ASSET RENAME\n   *\n   * @param {Object} asset Asset to rename\n   */\n  async assetRenamed (asset) {\n    WIKI.logger.info(`(STORAGE/DISK) Renaming file from ${asset.path} to ${asset.destinationPath}...`)\n    await fs.move(path.join(this.config.path, asset.path), path.join(this.config.path, asset.destinationPath), { overwrite: true })\n  },\n  async getLocalLocation (asset) {\n    return path.join(this.config.path, asset.path)\n  },\n  /**\n   * HANDLERS\n   */\n  async dump() {\n    WIKI.logger.info(`(STORAGE/DISK) Dumping all content to disk...`)\n\n    // -> Pages\n    await pipeline(\n      WIKI.models.knex.column('id', 'path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt', 'editorKey').select().from('pages').where({\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (page, enc, cb) => {\n          const pageObject = await WIKI.models.pages.query().findById(page.id)\n          page.tags = await pageObject.$relatedQuery('tags')\n\n          let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n          if (WIKI.config.lang.code !== page.localeCode) {\n            fileName = `${page.localeCode}/${fileName}`\n          }\n          WIKI.logger.info(`(STORAGE/DISK) Dumping page ${fileName}...`)\n          const filePath = path.join(this.config.path, fileName)\n          await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8')\n          cb()\n        }\n      })\n    )\n\n    // -> Assets\n    const assetFolders = await WIKI.models.assetFolders.getAllPaths()\n\n    await pipeline(\n      WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (asset, enc, cb) => {\n          const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename\n          WIKI.logger.info(`(STORAGE/DISK) Dumping asset ${filename}...`)\n          await fs.outputFile(path.join(this.config.path, filename), asset.data)\n          cb()\n        }\n      })\n    )\n\n    WIKI.logger.info('(STORAGE/DISK) All content was dumped to disk successfully.')\n  },\n  async backup() {\n    return this.sync({ manual: true })\n  },\n  async importAll() {\n    WIKI.logger.info(`(STORAGE/DISK) Importing all content from local disk folder to the DB...`)\n    await commonDisk.importFromDisk({\n      fullPath: this.config.path,\n      moduleName: 'DISK'\n    })\n    WIKI.logger.info('(STORAGE/DISK) Import completed.')\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/dropbox/definition.yml",
    "content": "key: dropbox\ntitle: Dropbox\ndescription: Dropbox is a file hosting service that offers cloud storage, file synchronization, personal cloud, and client software.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/dropbox.svg\nwebsite: https://dropbox.com\nprops:\n  appKey: String\n  appSecret: String\n"
  },
  {
    "path": "server/modules/storage/dropbox/storage.js",
    "content": "module.exports = {\n  async activated() {\n\n  },\n  async deactivated() {\n\n  },\n  async init() {\n\n  },\n  async created() {\n\n  },\n  async updated() {\n\n  },\n  async deleted() {\n\n  },\n  async renamed() {\n\n  },\n  async getLocalLocation () {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/gdrive/definition.yml",
    "content": "key: gdrive\ntitle: Google Drive\ndescription: Google Drive is a file storage and synchronization service developed by Google.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/google-drive.svg\nwebsite: https://www.google.com/drive/\nprops:\n  clientId: String\n  clientSecret: String\n"
  },
  {
    "path": "server/modules/storage/gdrive/storage.js",
    "content": "module.exports = {\n  async activated() {\n\n  },\n  async deactivated() {\n\n  },\n  async init() {\n\n  },\n  async created() {\n\n  },\n  async updated() {\n\n  },\n  async deleted() {\n\n  },\n  async renamed() {\n\n  },\n  async getLocalLocation () {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/git/definition.yml",
    "content": "key: git\ntitle: Git\ndescription: Git is a version control system for tracking changes in computer files and coordinating work on those files among multiple people.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/git-alt.svg\nwebsite: https://git-scm.com/\nisAvailable: true\nsupportedModes:\n  - sync\n  - push\n  - pull\ndefaultMode: sync\nschedule: PT5M\nprops:\n  authType:\n    type: String\n    default: 'ssh'\n    title: Authentication Type\n    hint: Use SSH for maximum security.\n    enum:\n      - 'basic'\n      - 'ssh'\n    order: 1\n  repoUrl:\n    type: String\n    title: Repository URI\n    hint: Git-compliant URI (e.g. git@github.com:org/repo.git for ssh, https://github.com/org/repo.git for basic)\n    order: 2\n  branch:\n    type: String\n    default: 'master'\n    hint: The branch to use during pull / push\n    order: 3\n  sshPrivateKeyMode:\n    type: String\n    title: SSH Private Key Mode\n    hint: SSH Authentication Only - The mode to use to load the private key. Fill in the corresponding field below.\n    order: 11\n    default: 'path'\n    enum:\n        - 'path'\n        - 'contents'\n  sshPrivateKeyPath:\n    type: String\n    title: A - SSH Private Key Path\n    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.\n    order: 12\n  sshPrivateKeyContent:\n    type: String\n    title: B - SSH Private Key Contents\n    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.\n    multiline: true\n    sensitive: true\n    order: 13\n  verifySSL:\n    type: Boolean\n    default: true\n    title: Verify SSL Certificate\n    hint: Some hosts requires SSL certificate checking to be disabled. Leave enabled for proper security.\n    order: 14\n  basicUsername:\n    type: String\n    title: Username\n    hint: Basic Authentication Only\n    order: 20\n  basicPassword:\n    type: String\n    title: Password / PAT\n    hint: Basic Authentication Only\n    sensitive: true\n    order: 21\n  defaultEmail:\n    type: String\n    title: Default Author Email\n    default: 'name@company.com'\n    hint: 'Used as fallback in case the author of the change is not present.'\n    order: 22\n  defaultName:\n    type: String\n    title: Default Author Name\n    default: 'John Smith'\n    hint: 'Used as fallback in case the author of the change is not present.'\n    order: 23\n  localRepoPath:\n    type: String\n    title: Local Repository Path\n    default: './data/repo'\n    hint: 'Path where the local git repository will be created.'\n    order: 30\n  alwaysNamespace:\n    type: Boolean\n    title: Always Locale Namespace\n    default: false\n    hint: 'Whether to put content from the primary language into a subfolder.'\n    order: 40\n  gitBinaryPath:\n    type: String\n    title: Git Binary Path\n    default: ''\n    hint: Optional - Absolute path to the Git binary, when not available in PATH. Leave empty to use the default PATH location (recommended).\n    order: 50\nactions:\n  - handler: syncUntracked\n    label: Add Untracked Changes\n    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.\n  - handler: sync\n    label: Force Sync\n    hint: Will trigger an immediate sync operation, regardless of the current sync schedule. The sync direction is respected.\n  - handler: importAll\n    label: Import Everything\n    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.\n  - handler: purge\n    label: Purge Local Repository\n    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.\n"
  },
  {
    "path": "server/modules/storage/git/storage.js",
    "content": "const path = require('path')\nconst sgit = require('simple-git')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\nconst klaw = require('klaw')\nconst os = require('os')\n\nconst pageHelper = require('../../../helpers/page')\nconst assetHelper = require('../../../helpers/asset')\nconst commonDisk = require('../disk/common')\n\n/* global WIKI */\n\nmodule.exports = {\n  git: null,\n  repoPath: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'repo'),\n  async activated() {\n    // not used\n  },\n  async deactivated() {\n    // not used\n  },\n  /**\n   * INIT\n   */\n  async init() {\n    WIKI.logger.info('(STORAGE/GIT) Initializing...')\n    this.repoPath = path.resolve(WIKI.ROOTPATH, this.config.localRepoPath)\n    await fs.ensureDir(this.repoPath)\n    this.git = sgit(this.repoPath, { maxConcurrentProcesses: 1 })\n\n    // Set custom binary path\n    if (!_.isEmpty(this.config.gitBinaryPath)) {\n      this.git.customBinary(this.config.gitBinaryPath)\n    }\n\n    // Initialize repo (if needed)\n    WIKI.logger.info('(STORAGE/GIT) Checking repository state...')\n    const isRepo = await this.git.checkIsRepo()\n    if (!isRepo) {\n      WIKI.logger.info('(STORAGE/GIT) Initializing local repository...')\n      await this.git.init()\n    }\n\n    // Disable quotePath, color output\n    // Link https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath\n    await this.git.raw(['config', '--local', 'core.quotepath', false])\n    await this.git.raw(['config', '--local', 'color.ui', false])\n\n    // Set default author\n    await this.git.raw(['config', '--local', 'user.email', this.config.defaultEmail])\n    await this.git.raw(['config', '--local', 'user.name', this.config.defaultName])\n\n    // Purge existing remotes\n    WIKI.logger.info('(STORAGE/GIT) Listing existing remotes...')\n    const remotes = await this.git.getRemotes()\n    if (remotes.length > 0) {\n      WIKI.logger.info('(STORAGE/GIT) Purging existing remotes...')\n      for (let remote of remotes) {\n        await this.git.removeRemote(remote.name)\n      }\n    }\n\n    // Add remote\n    WIKI.logger.info('(STORAGE/GIT) Setting SSL Verification config...')\n    await this.git.raw(['config', '--local', '--bool', 'http.sslVerify', _.toString(this.config.verifySSL)])\n    switch (this.config.authType) {\n      case 'ssh':\n        WIKI.logger.info('(STORAGE/GIT) Setting SSH Command config...')\n        if (this.config.sshPrivateKeyMode === 'contents') {\n          try {\n            this.config.sshPrivateKeyPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'secure/git-ssh.pem')\n            await fs.outputFile(this.config.sshPrivateKeyPath, this.config.sshPrivateKeyContent + os.EOL, {\n              encoding: 'utf8',\n              mode: 0o600\n            })\n          } catch (err) {\n            WIKI.logger.error(err)\n            throw err\n          }\n        }\n        await this.git.addConfig('core.sshCommand', `ssh -i \"${this.config.sshPrivateKeyPath}\" -o StrictHostKeyChecking=no`)\n        WIKI.logger.info('(STORAGE/GIT) Adding origin remote via SSH...')\n        await this.git.addRemote('origin', this.config.repoUrl)\n        break\n      default:\n        WIKI.logger.info('(STORAGE/GIT) Adding origin remote via HTTP/S...')\n        let originUrl = ''\n        if (_.startsWith(this.config.repoUrl, 'http')) {\n          originUrl = this.config.repoUrl.replace('://', `://${encodeURI(this.config.basicUsername)}:${encodeURI(this.config.basicPassword)}@`)\n        } else {\n          originUrl = `https://${encodeURI(this.config.basicUsername)}:${encodeURI(this.config.basicPassword)}@${this.config.repoUrl}`\n        }\n        await this.git.addRemote('origin', originUrl)\n        break\n    }\n\n    // Fetch updates for remote\n    WIKI.logger.info('(STORAGE/GIT) Fetch updates from remote...')\n    await this.git.raw(['remote', 'update', 'origin'])\n\n    // Checkout branch\n    const branches = await this.git.branch()\n    if (!_.includes(branches.all, this.config.branch) && !_.includes(branches.all, `remotes/origin/${this.config.branch}`)) {\n      throw new Error('Invalid branch! Make sure it exists on the remote first.')\n    }\n    WIKI.logger.info(`(STORAGE/GIT) Checking out branch ${this.config.branch}...`)\n    await this.git.checkout(this.config.branch)\n\n    // Perform initial sync\n    await this.sync()\n\n    WIKI.logger.info('(STORAGE/GIT) Initialization completed.')\n  },\n  /**\n   * SYNC\n   */\n  async sync() {\n    const currentCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch, '--']), 'latest', {})\n\n    const rootUser = await WIKI.models.users.getRootUser()\n\n    // Pull rebase\n    if (_.includes(['sync', 'pull'], this.mode)) {\n      WIKI.logger.info(`(STORAGE/GIT) Performing pull rebase from origin on branch ${this.config.branch}...`)\n      await this.git.pull('origin', this.config.branch, ['--rebase'])\n    }\n\n    // Push\n    if (_.includes(['sync', 'push'], this.mode)) {\n      WIKI.logger.info(`(STORAGE/GIT) Performing push to origin on branch ${this.config.branch}...`)\n      let pushOpts = ['--signed=if-asked']\n      if (this.mode === 'push') {\n        pushOpts.push('--force')\n      }\n      await this.git.push('origin', this.config.branch, pushOpts)\n    }\n\n    // Process Changes\n    if (_.includes(['sync', 'pull'], this.mode)) {\n      const latestCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch, '--']), 'latest', {})\n\n      const diff = await this.git.diffSummary(['-M', currentCommitLog.hash, latestCommitLog.hash])\n      if (_.get(diff, 'files', []).length > 0) {\n        let filesToProcess = []\n        const filePattern = /(.*?)(?:{(.*?))? => (?:(.*?)})?(.*)/\n        for (const f of diff.files) {\n          const fMatch = f.file.match(filePattern)\n          const fNames = {\n            old: null,\n            new: null\n          }\n          if (!fMatch) {\n            fNames.old = f.file\n            fNames.new = f.file\n          } else if (!fMatch[2] && !fMatch[3]) {\n            fNames.old = fMatch[1]\n            fNames.new = fMatch[4]\n          } else {\n            fNames.old = (fMatch[1] + fMatch[2] + fMatch[4]).replace('//', '/')\n            fNames.new = (fMatch[1] + fMatch[3] + fMatch[4]).replace('//', '/')\n          }\n          const fPath = path.join(this.repoPath, fNames.new)\n          let fStats = { size: 0 }\n          try {\n            fStats = await fs.stat(fPath)\n          } catch (err) {\n            if (err.code !== 'ENOENT') {\n              WIKI.logger.warn(`(STORAGE/GIT) Failed to access file ${f.file}! Skipping...`)\n              continue\n            }\n          }\n\n          filesToProcess.push({\n            ...f,\n            file: {\n              path: fPath,\n              stats: fStats\n            },\n            oldPath: fNames.old,\n            relPath: fNames.new\n          })\n        }\n        await this.processFiles(filesToProcess, rootUser)\n      }\n    }\n  },\n  /**\n   * Process Files\n   *\n   * @param {Array<String>} files Array of files to process\n   */\n  async processFiles(files, user) {\n    for (const item of files) {\n      const contentType = pageHelper.getContentType(item.relPath)\n      const fileExists = await fs.pathExists(item.file.path)\n      if (!item.binary && contentType) {\n        // -> Page\n\n        if (fileExists && !item.importAll && item.relPath !== item.oldPath) {\n          // Page was renamed by git, so rename in DB\n          WIKI.logger.info(`(STORAGE/GIT) Page marked as renamed: from ${item.oldPath} to ${item.relPath}`)\n\n          const contentPath = pageHelper.getPagePath(item.oldPath)\n          const contentDestinationPath = pageHelper.getPagePath(item.relPath)\n          await WIKI.models.pages.movePage({\n            user: user,\n            path: contentPath.path,\n            destinationPath: contentDestinationPath.path,\n            locale: contentPath.locale,\n            destinationLocale: contentPath.locale,\n            skipStorage: true\n          })\n        } else if (!fileExists && !item.importAll && item.deletions > 0 && item.insertions === 0) {\n          // Page was deleted by git, can safely mark as deleted in DB\n          WIKI.logger.info(`(STORAGE/GIT) Page marked as deleted: ${item.relPath}`)\n\n          const contentPath = pageHelper.getPagePath(item.relPath)\n          await WIKI.models.pages.deletePage({\n            user: user,\n            path: contentPath.path,\n            locale: contentPath.locale,\n            skipStorage: true\n          })\n          continue\n        }\n\n        try {\n          await commonDisk.processPage({\n            user,\n            relPath: item.relPath,\n            fullPath: this.repoPath,\n            contentType: contentType,\n            moduleName: 'GIT'\n          })\n        } catch (err) {\n          WIKI.logger.warn(`(STORAGE/GIT) Failed to process ${item.relPath}`)\n          WIKI.logger.warn(err)\n        }\n      } else {\n        // -> Asset\n\n        if (fileExists && !item.importAll && ((item.before === item.after) || (item.deletions === 0 && item.insertions === 0))) {\n          // Asset was renamed by git, so rename in DB\n          WIKI.logger.info(`(STORAGE/GIT) Asset marked as renamed: from ${item.oldPath} to ${item.relPath}`)\n\n          const fileHash = assetHelper.generateHash(item.relPath)\n          const assetToRename = await WIKI.models.assets.query().findOne({ hash: fileHash })\n          if (assetToRename) {\n            await WIKI.models.assets.query().patch({\n              filename: item.relPath,\n              hash: fileHash\n            }).findById(assetToRename.id)\n            await assetToRename.deleteAssetCache()\n          } else {\n            WIKI.logger.info(`(STORAGE/GIT) Asset was not found in the DB, nothing to rename: ${item.relPath}`)\n          }\n          continue\n        } else if (!fileExists && !item.importAll && ((item.before > 0 && item.after === 0) || (item.deletions > 0 && item.insertions === 0))) {\n          // Asset was deleted by git, can safely mark as deleted in DB\n          WIKI.logger.info(`(STORAGE/GIT) Asset marked as deleted: ${item.relPath}`)\n\n          const fileHash = assetHelper.generateHash(item.relPath)\n          const assetToDelete = await WIKI.models.assets.query().findOne({ hash: fileHash })\n          if (assetToDelete) {\n            await WIKI.models.knex('assetData').where('id', assetToDelete.id).del()\n            await WIKI.models.assets.query().deleteById(assetToDelete.id)\n            await assetToDelete.deleteAssetCache()\n          } else {\n            WIKI.logger.info(`(STORAGE/GIT) Asset was not found in the DB, nothing to delete: ${item.relPath}`)\n          }\n          continue\n        }\n\n        try {\n          await commonDisk.processAsset({\n            user,\n            relPath: item.relPath,\n            file: item.file,\n            contentType: contentType,\n            moduleName: 'GIT'\n          })\n        } catch (err) {\n          WIKI.logger.warn(`(STORAGE/GIT) Failed to process asset ${item.relPath}`)\n          WIKI.logger.warn(err)\n        }\n      }\n    }\n  },\n  /**\n   * CREATE\n   *\n   * @param {Object} page Page to create\n   */\n  async created(page) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing new file [${page.localeCode}] ${page.path}...`)\n    let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) {\n      fileName = `${page.localeCode}/${fileName}`\n    }\n    const filePath = path.join(this.repoPath, fileName)\n    await fs.outputFile(filePath, page.injectMetadata(), 'utf8')\n\n    const gitFilePath = `./${fileName}`\n    if ((await this.git.checkIgnore(gitFilePath)).length === 0) {\n      await this.git.add(gitFilePath)\n      await this.git.commit(`docs: create ${page.path}`, fileName, {\n        '--author': `\"${page.authorName} <${page.authorEmail}>\"`\n      })\n    }\n  },\n  /**\n   * UPDATE\n   *\n   * @param {Object} page Page to update\n   */\n  async updated(page) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing updated file [${page.localeCode}] ${page.path}...`)\n    let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) {\n      fileName = `${page.localeCode}/${fileName}`\n    }\n    const filePath = path.join(this.repoPath, fileName)\n    await fs.outputFile(filePath, page.injectMetadata(), 'utf8')\n\n    const gitFilePath = `./${fileName}`\n    if ((await this.git.checkIgnore(gitFilePath)).length === 0) {\n      await this.git.add(gitFilePath)\n      await this.git.commit(`docs: update ${page.path}`, fileName, {\n        '--author': `\"${page.authorName} <${page.authorEmail}>\"`\n      })\n    }\n  },\n  /**\n   * DELETE\n   *\n   * @param {Object} page Page to delete\n   */\n  async deleted(page) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing removed file [${page.localeCode}] ${page.path}...`)\n    let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) {\n      fileName = `${page.localeCode}/${fileName}`\n    }\n\n    const gitFilePath = `./${fileName}`\n    if ((await this.git.checkIgnore(gitFilePath)).length === 0) {\n      await this.git.rm(gitFilePath)\n      await this.git.commit(`docs: delete ${page.path}`, fileName, {\n        '--author': `\"${page.authorName} <${page.authorEmail}>\"`\n      })\n    }\n  },\n  /**\n   * RENAME\n   *\n   * @param {Object} page Page to rename\n   */\n  async renamed(page) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing file move from [${page.localeCode}] ${page.path} to [${page.destinationLocaleCode}] ${page.destinationPath}...`)\n    let sourceFileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n    let destinationFileName = `${page.destinationPath}.${pageHelper.getFileExtension(page.contentType)}`\n\n    if (this.config.alwaysNamespace || WIKI.config.lang.namespacing) {\n      if (this.config.alwaysNamespace || WIKI.config.lang.code !== page.localeCode) {\n        sourceFileName = `${page.localeCode}/${sourceFileName}`\n      }\n      if (this.config.alwaysNamespace || WIKI.config.lang.code !== page.destinationLocaleCode) {\n        destinationFileName = `${page.destinationLocaleCode}/${destinationFileName}`\n      }\n    }\n\n    const sourceFilePath = path.join(this.repoPath, sourceFileName)\n    const destinationFilePath = path.join(this.repoPath, destinationFileName)\n    await fs.move(sourceFilePath, destinationFilePath)\n\n    await this.git.rm(`./${sourceFileName}`)\n    await this.git.add(`./${destinationFileName}`)\n    await this.git.commit(`docs: rename ${page.path} to ${page.destinationPath}`, [sourceFilePath, destinationFilePath], {\n      '--author': `\"${page.moveAuthorName} <${page.moveAuthorEmail}>\"`\n    })\n  },\n  /**\n   * ASSET UPLOAD\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetUploaded (asset) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing new file ${asset.path}...`)\n    const filePath = path.join(this.repoPath, asset.path)\n    await fs.outputFile(filePath, asset.data, 'utf8')\n\n    await this.git.add(`./${asset.path}`)\n    await this.git.commit(`docs: upload ${asset.path}`, asset.path, {\n      '--author': `\"${asset.authorName} <${asset.authorEmail}>\"`\n    })\n  },\n  /**\n   * ASSET DELETE\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetDeleted (asset) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing removed file ${asset.path}...`)\n\n    await this.git.rm(`./${asset.path}`)\n    await this.git.commit(`docs: delete ${asset.path}`, asset.path, {\n      '--author': `\"${asset.authorName} <${asset.authorEmail}>\"`\n    })\n  },\n  /**\n   * ASSET RENAME\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetRenamed (asset) {\n    WIKI.logger.info(`(STORAGE/GIT) Committing file move from ${asset.path} to ${asset.destinationPath}...`)\n\n    await this.git.mv(`./${asset.path}`, `./${asset.destinationPath}`)\n    await this.git.commit(`docs: rename ${asset.path} to ${asset.destinationPath}`, [asset.path, asset.destinationPath], {\n      '--author': `\"${asset.moveAuthorName} <${asset.moveAuthorEmail}>\"`\n    })\n  },\n  async getLocalLocation (asset) {\n    return path.join(this.repoPath, asset.path)\n  },\n  /**\n   * HANDLERS\n   */\n  async importAll() {\n    WIKI.logger.info(`(STORAGE/GIT) Importing all content from local Git repo to the DB...`)\n\n    const rootUser = await WIKI.models.users.getRootUser()\n\n    await pipeline(\n      klaw(this.repoPath, {\n        filter: (f) => {\n          return !_.includes(f, '.git')\n        }\n      }),\n      new Transform({\n        objectMode: true,\n        transform: async (file, enc, cb) => {\n          const relPath = file.path.substr(this.repoPath.length + 1)\n          if (file.stats.size < 1) {\n            // Skip directories and zero-byte files\n            return cb()\n          } else if (relPath && relPath.length > 3) {\n            WIKI.logger.info(`(STORAGE/GIT) Processing ${relPath}...`)\n            await this.processFiles([{\n              user: rootUser,\n              relPath,\n              file,\n              deletions: 0,\n              insertions: 0,\n              importAll: true\n            }], rootUser)\n          }\n          cb()\n        }\n      })\n    )\n\n    commonDisk.clearFolderCache()\n\n    WIKI.logger.info('(STORAGE/GIT) Import completed.')\n  },\n  async syncUntracked() {\n    WIKI.logger.info(`(STORAGE/GIT) Adding all untracked content...`)\n\n    // -> Pages\n    await pipeline(\n      WIKI.models.knex.column('id', 'path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt', 'editorKey').select().from('pages').where({\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (page, enc, cb) => {\n          const pageObject = await WIKI.models.pages.query().findById(page.id)\n          page.tags = await pageObject.$relatedQuery('tags')\n\n          let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`\n          if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) {\n            fileName = `${page.localeCode}/${fileName}`\n          }\n          WIKI.logger.info(`(STORAGE/GIT) Adding page ${fileName}...`)\n          const filePath = path.join(this.repoPath, fileName)\n          await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8')\n          await this.git.add(`./${fileName}`)\n          cb()\n        }\n      })\n    )\n\n    // -> Assets\n    const assetFolders = await WIKI.models.assetFolders.getAllPaths()\n\n    await pipeline(\n      WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (asset, enc, cb) => {\n          const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename\n          WIKI.logger.info(`(STORAGE/GIT) Adding asset ${filename}...`)\n          await fs.outputFile(path.join(this.repoPath, filename), asset.data)\n          await this.git.add(`./${filename}`)\n          cb()\n        }\n      })\n    )\n\n    await this.git.commit(`docs: add all untracked content`)\n    WIKI.logger.info('(STORAGE/GIT) All content is now tracked.')\n  },\n  async purge() {\n    WIKI.logger.info(`(STORAGE/GIT) Purging local repository...`)\n    await fs.emptyDir(this.repoPath)\n    WIKI.logger.info('(STORAGE/GIT) Local repository is now empty. Reinitializing...')\n    await this.init()\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/onedrive/definition.yml",
    "content": "key: onedrive\ntitle: OneDrive\ndescription: OneDrive is a file hosting service operated by Microsoft as part of its suite of Office Online services.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/onedrive.svg\nwebsite: https://onedrive.live.com/about/\nprops:\n  clientId: String\n  clientSecret: String\n"
  },
  {
    "path": "server/modules/storage/onedrive/storage.js",
    "content": "module.exports = {\n  async activated() {\n\n  },\n  async deactivated() {\n\n  },\n  async init() {\n\n  },\n  async created() {\n\n  },\n  async updated() {\n\n  },\n  async deleted() {\n\n  },\n  async renamed() {\n\n  },\n  async getLocalLocation () {\n\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/s3/common.js",
    "content": "const S3 = require('aws-sdk/clients/s3')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\nconst _ = require('lodash')\nconst pageHelper = require('../../../helpers/page.js')\n\n/* global WIKI */\n\n/**\n * Deduce the file path given the `page` object and the object's key to the page's path.\n */\nconst getFilePath = (page, pathKey) => {\n  const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}`\n  const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode\n  return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName\n}\n\n/**\n * Can be used with S3 compatible storage.\n */\nmodule.exports = class S3CompatibleStorage {\n  constructor(storageName) {\n    this.storageName = storageName\n    this.bucketName = ''\n  }\n  async activated() {\n    // not used\n  }\n  async deactivated() {\n    // not used\n  }\n  async init() {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Initializing...`)\n    const { accessKeyId, secretAccessKey, bucket } = this.config\n    const s3Config = {\n      accessKeyId,\n      secretAccessKey,\n      params: { Bucket: bucket },\n      apiVersions: '2006-03-01'\n    }\n\n    if (!_.isNil(this.config.region)) {\n      s3Config.region = this.config.region\n    }\n    if (!_.isNil(this.config.endpoint)) {\n      s3Config.endpoint = this.config.endpoint\n    }\n    if (!_.isNil(this.config.sslEnabled)) {\n      s3Config.sslEnabled = this.config.sslEnabled\n    }\n    if (!_.isNil(this.config.s3ForcePathStyle)) {\n      s3Config.s3ForcePathStyle = this.config.s3ForcePathStyle\n    }\n    if (!_.isNil(this.config.s3BucketEndpoint)) {\n      s3Config.s3BucketEndpoint = this.config.s3BucketEndpoint\n    }\n\n    this.s3 = new S3(s3Config)\n    this.bucketName = bucket\n\n    // determine if a bucket exists and you have permission to access it\n    await this.s3.headBucket().promise()\n\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Initialization completed.`)\n  }\n  async created(page) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Creating file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    await this.s3.putObject({ Key: filePath, Body: page.injectMetadata() }).promise()\n  }\n  async updated(page) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Updating file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    await this.s3.putObject({ Key: filePath, Body: page.injectMetadata() }).promise()\n  }\n  async deleted(page) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Deleting file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    await this.s3.deleteObject({ Key: filePath }).promise()\n  }\n  async renamed(page) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file ${page.path} to ${page.destinationPath}...`)\n    let sourceFilePath = getFilePath(page, 'path')\n    let destinationFilePath = getFilePath(page, 'destinationPath')\n    if (WIKI.config.lang.namespacing) {\n      if (WIKI.config.lang.code !== page.localeCode) {\n        sourceFilePath = `${page.localeCode}/${sourceFilePath}`\n      }\n      if (WIKI.config.lang.code !== page.destinationLocaleCode) {\n        destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`\n      }\n    }\n    await this.s3.copyObject({ CopySource: `${this.bucketName}/${sourceFilePath}`, Key: destinationFilePath }).promise()\n    await this.s3.deleteObject({ Key: sourceFilePath }).promise()\n  }\n  /**\n   * ASSET UPLOAD\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetUploaded (asset) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Creating new file ${asset.path}...`)\n    await this.s3.putObject({ Key: asset.path, Body: asset.data }).promise()\n  }\n  /**\n   * ASSET DELETE\n   *\n   * @param {Object} asset Asset to delete\n   */\n  async assetDeleted (asset) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Deleting file ${asset.path}...`)\n    await this.s3.deleteObject({ Key: asset.path }).promise()\n  }\n  /**\n   * ASSET RENAME\n   *\n   * @param {Object} asset Asset to rename\n   */\n  async assetRenamed (asset) {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file from ${asset.path} to ${asset.destinationPath}...`)\n    await this.s3.copyObject({ CopySource: `${this.bucketName}/${asset.path}`, Key: asset.destinationPath }).promise()\n    await this.s3.deleteObject({ Key: asset.path }).promise()\n  }\n  async getLocalLocation () {\n\n  }\n  /**\n   * HANDLERS\n   */\n  async exportAll() {\n    WIKI.logger.info(`(STORAGE/${this.storageName}) Exporting all content to the cloud provider...`)\n\n    // -> Pages\n    await pipeline(\n      WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt').select().from('pages').where({\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (page, enc, cb) => {\n          const filePath = getFilePath(page, 'path')\n          WIKI.logger.info(`(STORAGE/${this.storageName}) Adding page ${filePath}...`)\n          await this.s3.putObject({ Key: filePath, Body: pageHelper.injectPageMetadata(page) }).promise()\n          cb()\n        }\n      })\n    )\n\n    // -> Assets\n    const assetFolders = await WIKI.models.assetFolders.getAllPaths()\n\n    await pipeline(\n      WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (asset, enc, cb) => {\n          const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename\n          WIKI.logger.info(`(STORAGE/${this.storageName}) Adding asset ${filename}...`)\n          await this.s3.putObject({ Key: filename, Body: asset.data }).promise()\n          cb()\n        }\n      })\n    )\n\n    WIKI.logger.info(`(STORAGE/${this.storageName}) All content has been pushed to the cloud provider.`)\n  }\n}\n"
  },
  {
    "path": "server/modules/storage/s3/definition.yml",
    "content": "key: s3\ntitle: Amazon S3\ndescription: Amazon S3 is a cloud computing web service offered by Amazon Web Services which provides object storage.\nauthor: andrewsim\nlogo: https://static.requarks.io/logo/aws-s3.svg\nwebsite: https://aws.amazon.com/s3/\nisAvailable: true\nsupportedModes:\n  - push\ndefaultMode: push\nschedule: false\nprops:\n  region:\n    type: String\n    title: Region\n    hint: The AWS datacenter region where the bucket will be created.\n    order: 1\n  bucket:\n    type: String\n    title: Unique bucket name\n    hint: The unique bucket name to create (e.g. wiki-johndoe).\n    order: 2\n  accessKeyId:\n    type: String\n    title: Access Key ID\n    hint: The Access Key.\n    order: 3\n  secretAccessKey:\n    type: String\n    title: Secret Access Key\n    hint: The Secret Access Key for the Access Key ID you created above.\n    sensitive: true\n    order: 4\nactions:\n  - handler: exportAll\n    label: Export All\n    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.\n"
  },
  {
    "path": "server/modules/storage/s3/storage.js",
    "content": "const S3CompatibleStorage = require('./common')\n\nmodule.exports = new S3CompatibleStorage('S3')\n"
  },
  {
    "path": "server/modules/storage/s3generic/definition.yml",
    "content": "key: s3generic\ntitle: S3 Generic\ndescription: Generic storage module for S3-compatible services.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/aws-s3-alt.svg\nwebsite: https://wiki.js.org\nisAvailable: true\nsupportedModes:\n  - push\ndefaultMode: push\nschedule: false\nprops:\n  endpoint:\n    type: String\n    title: Endpoint URI\n    hint: The full S3-compliant endpoint URI.\n    default: https://service.region.example.com\n    order: 1\n  bucket:\n    type: String\n    title: Unique bucket name\n    hint: The unique bucket name to create (e.g. wiki-johndoe)\n    order: 2\n  accessKeyId:\n    type: String\n    title: Access Key ID\n    hint: The Access Key ID.\n    order: 3\n  secretAccessKey:\n    type: String\n    title: Access Key Secret\n    hint: The Access Key Secret for the Access Key ID above.\n    sensitive: true\n    order: 4\n  sslEnabled:\n    type: Boolean\n    title: Use SSL\n    hint: Whether to enable SSL for requests\n    default: true\n    order: 5\n  s3ForcePathStyle:\n    type: Boolean\n    title: Force Path Style for S3 objects\n    hint: Whether to force path style URLs for S3 objects.\n    default: false\n    order: 6\n  s3BucketEndpoint:\n    type: Boolean\n    title: Single Bucket Endpoint\n    hint: Whether the provided endpoint addresses an individual bucket.\n    default: false\n    order: 7\nactions:\n  - handler: exportAll\n    label: Export All\n    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.\n\n"
  },
  {
    "path": "server/modules/storage/s3generic/storage.js",
    "content": "const S3CompatibleStorage = require('../s3/common')\n\nmodule.exports = new S3CompatibleStorage('S3Generic')\n"
  },
  {
    "path": "server/modules/storage/sftp/definition.yml",
    "content": "key: sftp\ntitle: SFTP\ndescription: 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.\nauthor: requarks.io\nlogo: https://static.requarks.io/logo/ssh.svg\nwebsite: https://www.ssh.com/ssh/sftp\nisAvailable: true\nsupportedModes:\n  - push\ndefaultMode: push\nschedule: false\nprops:\n  host:\n    type: String\n    title: Host\n    default: ''\n    hint: Hostname or IP of the remote SSH server.\n    order: 1\n  port:\n    type: Number\n    title: Port\n    default: 22\n    hint: SSH port of the remote server.\n    order: 2\n  authMode:\n    type: String\n    title: Authentication Method\n    default: 'privateKey'\n    hint: Whether to use Private Key or Password-based authentication. A private key is highly recommended for best security.\n    enum:\n      - privateKey\n      - password\n    order: 3\n  username:\n    type: String\n    title: Username\n    default: ''\n    hint: Username for authentication.\n    order: 4\n  privateKey:\n    type: String\n    title: Private Key Contents\n    default: ''\n    hint: (Private Key Authentication Only) - Contents of the private key\n    multiline: true\n    sensitive: true\n    order: 5\n  passphrase:\n    type: String\n    title: Private Key Passphrase\n    default: ''\n    hint: (Private Key Authentication Only) - Passphrase if the private key is encrypted, leave empty otherwise\n    sensitive: true\n    order: 6\n  password:\n    type: String\n    title: Password\n    default: ''\n    hint: (Password-based Authentication Only) - Password for authentication\n    sensitive: true\n    order: 6\n  basePath:\n    type: String\n    title: Base Directory Path\n    default: '/root/wiki'\n    hint: Base directory where files will be transferred to. The path must already exists and be writable by the user.\nactions:\n  - handler: exportAll\n    label: Export All\n    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.\n\n"
  },
  {
    "path": "server/modules/storage/sftp/storage.js",
    "content": "const SSH2Promise = require('ssh2-promise')\nconst _ = require('lodash')\nconst path = require('path')\nconst { pipeline } = require('node:stream/promises')\nconst { Transform } = require('node:stream')\nconst pageHelper = require('../../../helpers/page.js')\n\n/* global WIKI */\n\nconst getFilePath = (page, pathKey) => {\n  const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}`\n  const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode\n  return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName\n}\n\nmodule.exports = {\n  client: null,\n  sftp: null,\n  async activated() {\n\n  },\n  async deactivated() {\n\n  },\n  async init() {\n    WIKI.logger.info(`(STORAGE/SFTP) Initializing...`)\n    this.client = new SSH2Promise({\n      host: this.config.host,\n      port: this.config.port || 22,\n      username: this.config.username,\n      password: (this.config.authMode === 'password') ? this.config.password : null,\n      privateKey: (this.config.authMode === 'privateKey') ? this.config.privateKey : null,\n      passphrase: (this.config.authMode === 'privateKey') ? this.config.passphrase : null\n    })\n    await this.client.connect()\n    this.sftp = this.client.sftp()\n    try {\n      await this.sftp.readdir(this.config.basePath)\n    } catch (err) {\n      WIKI.logger.warn(`(STORAGE/SFTP) ${err.message}`)\n      throw new Error(`Unable to read specified base directory: ${err.message}`)\n    }\n    WIKI.logger.info(`(STORAGE/SFTP) Initialization completed.`)\n  },\n  async created(page) {\n    WIKI.logger.info(`(STORAGE/SFTP) Creating file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    await this.ensureDirectory(filePath)\n    await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), page.injectMetadata())\n  },\n  async updated(page) {\n    WIKI.logger.info(`(STORAGE/SFTP) Updating file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    await this.ensureDirectory(filePath)\n    await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), page.injectMetadata())\n  },\n  async deleted(page) {\n    WIKI.logger.info(`(STORAGE/SFTP) Deleting file ${page.path}...`)\n    const filePath = getFilePath(page, 'path')\n    await this.sftp.unlink(path.posix.join(this.config.basePath, filePath))\n  },\n  async renamed(page) {\n    WIKI.logger.info(`(STORAGE/SFTP) Renaming file ${page.path} to ${page.destinationPath}...`)\n    let sourceFilePath = getFilePath(page, 'path')\n    let destinationFilePath = getFilePath(page, 'destinationPath')\n    if (WIKI.config.lang.namespacing) {\n      if (WIKI.config.lang.code !== page.localeCode) {\n        sourceFilePath = `${page.localeCode}/${sourceFilePath}`\n      }\n      if (WIKI.config.lang.code !== page.destinationLocaleCode) {\n        destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`\n      }\n    }\n    await this.ensureDirectory(destinationFilePath)\n    await this.sftp.rename(path.posix.join(this.config.basePath, sourceFilePath), path.posix.join(this.config.basePath, destinationFilePath))\n  },\n  /**\n   * ASSET UPLOAD\n   *\n   * @param {Object} asset Asset to upload\n   */\n  async assetUploaded (asset) {\n    WIKI.logger.info(`(STORAGE/SFTP) Creating new file ${asset.path}...`)\n    await this.ensureDirectory(asset.path)\n    await this.sftp.writeFile(path.posix.join(this.config.basePath, asset.path), asset.data)\n  },\n  /**\n   * ASSET DELETE\n   *\n   * @param {Object} asset Asset to delete\n   */\n  async assetDeleted (asset) {\n    WIKI.logger.info(`(STORAGE/SFTP) Deleting file ${asset.path}...`)\n    await this.sftp.unlink(path.posix.join(this.config.basePath, asset.path))\n  },\n  /**\n   * ASSET RENAME\n   *\n   * @param {Object} asset Asset to rename\n   */\n  async assetRenamed (asset) {\n    WIKI.logger.info(`(STORAGE/SFTP) Renaming file from ${asset.path} to ${asset.destinationPath}...`)\n    await this.ensureDirectory(asset.destinationPath)\n    await this.sftp.rename(path.posix.join(this.config.basePath, asset.path), path.posix.join(this.config.basePath, asset.destinationPath))\n  },\n  async getLocalLocation () {\n\n  },\n  /**\n   * HANDLERS\n   */\n  async exportAll() {\n    WIKI.logger.info(`(STORAGE/SFTP) Exporting all content to the remote server...`)\n\n    // -> Pages\n    await pipeline(\n      WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt').select().from('pages').where({\n        isPrivate: false\n      }).stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (page, enc, cb) => {\n          const filePath = getFilePath(page, 'path')\n          WIKI.logger.info(`(STORAGE/SFTP) Adding page ${filePath}...`)\n          await this.ensureDirectory(filePath)\n          await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), pageHelper.injectPageMetadata(page))\n          cb()\n        }\n      })\n    )\n\n    // -> Assets\n    const assetFolders = await WIKI.models.assetFolders.getAllPaths()\n\n    await pipeline(\n      WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),\n      new Transform({\n        objectMode: true,\n        transform: async (asset, enc, cb) => {\n          const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename\n          WIKI.logger.info(`(STORAGE/SFTP) Adding asset ${filename}...`)\n          await this.ensureDirectory(filename)\n          await this.sftp.writeFile(path.posix.join(this.config.basePath, filename), asset.data)\n          cb()\n        }\n      })\n    )\n\n    WIKI.logger.info('(STORAGE/SFTP) All content has been pushed to the remote server.')\n  },\n  async ensureDirectory(filePath) {\n    if (filePath.indexOf('/') >= 0) {\n      try {\n        const folderPaths = _.dropRight(filePath.split('/'))\n        for (let i = 1; i <= folderPaths.length; i++) {\n          const folderSection = _.take(folderPaths, i).join('/')\n          const folderDir = path.posix.join(this.config.basePath, folderSection)\n          try {\n            await this.sftp.readdir(folderDir)\n          } catch (err) {\n            await this.sftp.mkdir(folderDir)\n          }\n        }\n      } catch (err) {}\n    }\n  }\n}\n"
  },
  {
    "path": "server/setup.js",
    "content": "const path = require('path')\nconst { v4: uuid } = require('uuid')\nconst bodyParser = require('body-parser')\nconst compression = require('compression')\nconst express = require('express')\nconst favicon = require('serve-favicon')\nconst http = require('http')\nconst fs = require('fs-extra')\nconst _ = require('lodash')\nconst crypto = require('crypto')\nconst pem2jwk = require('pem-jwk').pem2jwk\nconst semver = require('semver')\n\nconst randomBytesAsync = require('util').promisify(crypto.randomBytes)\n\n/* global WIKI */\n\nmodule.exports = () => {\n  WIKI.config.site = {\n    path: '',\n    title: 'Wiki.js'\n  }\n\n  WIKI.system = require('./core/system')\n\n  // ----------------------------------------\n  // Define Express App\n  // ----------------------------------------\n\n  let app = express()\n  app.use(compression())\n\n  // ----------------------------------------\n  // Public Assets\n  // ----------------------------------------\n\n  app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))\n  app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets')))\n\n  // ----------------------------------------\n  // View Engine Setup\n  // ----------------------------------------\n\n  app.set('views', path.join(WIKI.SERVERPATH, 'views'))\n  app.set('view engine', 'pug')\n\n  app.use(bodyParser.json())\n  app.use(bodyParser.urlencoded({ extended: false }))\n\n  app.locals.config = WIKI.config\n  app.locals.data = WIKI.data\n  app.locals._ = require('lodash')\n  app.locals.devMode = WIKI.devMode\n\n  // ----------------------------------------\n  // HMR (Dev Mode Only)\n  // ----------------------------------------\n\n  if (global.DEV) {\n    app.use(global.WP_DEV.devMiddleware)\n    app.use(global.WP_DEV.hotMiddleware)\n  }\n\n  // ----------------------------------------\n  // Controllers\n  // ----------------------------------------\n\n  app.get('*', async (req, res) => {\n    let packageObj = await fs.readJson(path.join(WIKI.ROOTPATH, 'package.json'))\n    res.render('setup', { packageObj })\n  })\n\n  /**\n   * Finalize\n   */\n  app.post('/finalize', async (req, res) => {\n    try {\n      // Set config\n      _.set(WIKI.config, 'auth', {\n        audience: 'urn:wiki.js',\n        tokenExpiration: '30m',\n        tokenRenewal: '14d'\n      })\n      _.set(WIKI.config, 'company', '')\n      _.set(WIKI.config, 'features', {\n        featurePageRatings: true,\n        featurePageComments: true,\n        featurePersonalWikis: true\n      })\n      _.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io')\n      _.set(WIKI.config, 'host', req.body.siteUrl)\n      _.set(WIKI.config, 'lang', {\n        code: 'en',\n        autoUpdate: true,\n        namespacing: false,\n        namespaces: []\n      })\n      _.set(WIKI.config, 'logo', {\n        hasLogo: false,\n        logoIsSquare: false\n      })\n      _.set(WIKI.config, 'mail', {\n        senderName: '',\n        senderEmail: '',\n        host: '',\n        port: 465,\n        name: '',\n        secure: true,\n        verifySSL: true,\n        user: '',\n        pass: '',\n        useDKIM: false,\n        dkimDomainName: '',\n        dkimKeySelector: '',\n        dkimPrivateKey: ''\n      })\n      _.set(WIKI.config, 'seo', {\n        description: '',\n        robots: ['index', 'follow'],\n        analyticsService: '',\n        analyticsId: ''\n      })\n      _.set(WIKI.config, 'sessionSecret', (await randomBytesAsync(32)).toString('hex'))\n      _.set(WIKI.config, 'telemetry', {\n        isEnabled: req.body.telemetry === true,\n        clientId: uuid()\n      })\n      _.set(WIKI.config, 'theming', {\n        theme: 'default',\n        darkMode: false,\n        iconset: 'mdi',\n        injectCSS: '',\n        injectHead: '',\n        injectBody: ''\n      })\n      _.set(WIKI.config, 'title', 'Wiki.js')\n\n      // Init Telemetry\n      WIKI.kernel.initTelemetry()\n      // WIKI.telemetry.sendEvent('setup', 'install-start')\n\n      // Basic checks\n      if (!semver.satisfies(process.version, '>=10.12')) {\n        throw new Error('Node.js 10.12.x or later required!')\n      }\n\n      // Create directory structure\n      WIKI.logger.info('Creating data directories...')\n      await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath))\n      await fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))\n      await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))\n\n      // Generate certificates\n      WIKI.logger.info('Generating certificates...')\n      const certs = crypto.generateKeyPairSync('rsa', {\n        modulusLength: 2048,\n        publicKeyEncoding: {\n          type: 'pkcs1',\n          format: 'pem'\n        },\n        privateKeyEncoding: {\n          type: 'pkcs1',\n          format: 'pem',\n          cipher: 'aes-256-cbc',\n          passphrase: WIKI.config.sessionSecret\n        }\n      })\n\n      _.set(WIKI.config, 'certs', {\n        jwk: pem2jwk(certs.publicKey),\n        public: certs.publicKey,\n        private: certs.privateKey\n      })\n\n      // Save config to DB\n      WIKI.logger.info('Persisting config to DB...')\n      await WIKI.configSvc.saveToDb([\n        'auth',\n        'certs',\n        'company',\n        'features',\n        'graphEndpoint',\n        'host',\n        'lang',\n        'logo',\n        'mail',\n        'seo',\n        'sessionSecret',\n        'telemetry',\n        'theming',\n        'uploads',\n        'title'\n      ], false)\n\n      // Truncate tables (reset from previous failed install)\n      await WIKI.models.locales.query().where('code', '!=', 'x').del()\n      await WIKI.models.navigation.query().truncate()\n      switch (WIKI.config.db.type) {\n        case 'postgres':\n          await WIKI.models.knex.raw('TRUNCATE groups, users CASCADE')\n          break\n        case 'mysql':\n        case 'mariadb':\n          await WIKI.models.groups.query().where('id', '>', 0).del()\n          await WIKI.models.users.query().where('id', '>', 0).del()\n          await WIKI.models.knex.raw('ALTER TABLE `groups` AUTO_INCREMENT = 1')\n          await WIKI.models.knex.raw('ALTER TABLE `users` AUTO_INCREMENT = 1')\n          break\n        case 'mssql':\n          await WIKI.models.groups.query().del()\n          await WIKI.models.users.query().del()\n          await WIKI.models.knex.raw(`\n            IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'groups' AND last_value IS NOT NULL)\n              DBCC CHECKIDENT ([groups], RESEED, 0)\n          `)\n          await WIKI.models.knex.raw(`\n            IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'users' AND last_value IS NOT NULL)\n              DBCC CHECKIDENT ([users], RESEED, 0)\n          `)\n          break\n        case 'sqlite':\n          await WIKI.models.groups.query().truncate()\n          await WIKI.models.users.query().truncate()\n          break\n      }\n\n      // Create default locale\n      WIKI.logger.info('Installing default locale...')\n      await WIKI.models.locales.query().insert({\n        code: 'en',\n        strings: {},\n        isRTL: false,\n        name: 'English',\n        nativeName: 'English'\n      })\n\n      // Create default groups\n\n      WIKI.logger.info('Creating default groups...')\n      const adminGroup = await WIKI.models.groups.query().insert({\n        name: 'Administrators',\n        permissions: JSON.stringify(['manage:system']),\n        pageRules: JSON.stringify([]),\n        isSystem: true\n      })\n      const guestGroup = await WIKI.models.groups.query().insert({\n        name: 'Guests',\n        permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),\n        pageRules: JSON.stringify([\n          { id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] }\n        ]),\n        isSystem: true\n      })\n      if (adminGroup.id !== 1 || guestGroup.id !== 2) {\n        throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')\n      }\n\n      // Load local authentication strategy\n      await WIKI.models.authentication.query().insert({\n        key: 'local',\n        config: {},\n        selfRegistration: false,\n        isEnabled: true,\n        domainWhitelist: {v: []},\n        autoEnrollGroups: {v: []},\n        order: 0,\n        strategyKey: 'local',\n        displayName: 'Local'\n      })\n\n      // Load editors + enable default\n      await WIKI.models.editors.refreshEditorsFromDisk()\n      await WIKI.models.editors.query().patch({ isEnabled: true }).where('key', 'markdown')\n\n      // Load loggers\n      await WIKI.models.loggers.refreshLoggersFromDisk()\n\n      // Load renderers\n      await WIKI.models.renderers.refreshRenderersFromDisk()\n\n      // Load search engines + enable default\n      await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()\n      await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')\n\n      // WIKI.telemetry.sendEvent('setup', 'install-loadedmodules')\n\n      // Load storage targets\n      await WIKI.models.storage.refreshTargetsFromDisk()\n\n      // Create root administrator\n      WIKI.logger.info('Creating root administrator...')\n      const adminUser = await WIKI.models.users.query().insert({\n        email: req.body.adminEmail.toLowerCase(),\n        provider: 'local',\n        password: req.body.adminPassword,\n        name: 'Administrator',\n        locale: 'en',\n        defaultEditor: 'markdown',\n        tfaIsActive: false,\n        isActive: true,\n        isVerified: true\n      })\n      await adminUser.$relatedQuery('groups').relate(adminGroup.id)\n\n      // Create Guest account\n      WIKI.logger.info('Creating guest account...')\n      const guestUser = await WIKI.models.users.query().insert({\n        provider: 'local',\n        email: 'guest@example.com',\n        name: 'Guest',\n        password: '',\n        locale: 'en',\n        defaultEditor: 'markdown',\n        tfaIsActive: false,\n        isSystem: true,\n        isActive: true,\n        isVerified: true\n      })\n      await guestUser.$relatedQuery('groups').relate(guestGroup.id)\n      if (adminUser.id !== 1 || guestUser.id !== 2) {\n        throw new Error('Incorrect users auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')\n      }\n\n      // Create site nav\n\n      WIKI.logger.info('Creating default site navigation')\n      await WIKI.models.navigation.query().insert({\n        key: 'site',\n        config: [\n          {\n            locale: 'en',\n            items: [\n              {\n                id: uuid(),\n                icon: 'mdi-home',\n                kind: 'link',\n                label: 'Home',\n                target: '/',\n                targetType: 'home',\n                visibilityMode: 'all',\n                visibilityGroups: null\n              }\n            ]\n          }\n        ]\n      })\n\n      WIKI.logger.info('Setup is complete!')\n      // WIKI.telemetry.sendEvent('setup', 'install-completed')\n      res.json({\n        ok: true,\n        redirectPath: '/',\n        redirectPort: WIKI.config.port\n      }).end()\n\n      if (WIKI.config.telemetry.isEnabled) {\n        await WIKI.telemetry.sendInstanceEvent('INSTALL')\n      }\n\n      WIKI.config.setup = false\n\n      WIKI.logger.info('Stopping Setup...')\n      WIKI.server.destroy(() => {\n        WIKI.logger.info('Setup stopped. Starting Wiki.js...')\n        _.delay(() => {\n          WIKI.kernel.bootMaster()\n        }, 1000)\n      })\n    } catch (err) {\n      try {\n        await WIKI.models.knex('settings').truncate()\n      } catch (err) {}\n      WIKI.telemetry.sendError(err)\n      res.json({ ok: false, error: err.message })\n    }\n  })\n\n  // ----------------------------------------\n  // Error handling\n  // ----------------------------------------\n\n  app.use(function (req, res, next) {\n    const err = new Error('Not Found')\n    err.status = 404\n    next(err)\n  })\n\n  app.use(function (err, req, res, next) {\n    res.status(err.status || 500)\n    res.send({\n      message: err.message,\n      error: WIKI.IS_DEBUG ? err : {}\n    })\n    WIKI.logger.error(err.message)\n    WIKI.telemetry.sendError(err)\n  })\n\n  // ----------------------------------------\n  // Start HTTP server\n  // ----------------------------------------\n\n  WIKI.logger.info(`Starting HTTP server on port ${WIKI.config.port}...`)\n\n  app.set('port', WIKI.config.port)\n\n  WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)\n  WIKI.server = http.createServer(app)\n  WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)\n\n  var openConnections = []\n\n  WIKI.server.on('connection', (conn) => {\n    let key = conn.remoteAddress + ':' + conn.remotePort\n    openConnections[key] = conn\n    conn.on('close', () => {\n      openConnections.splice(key, 1)\n    })\n  })\n\n  WIKI.server.destroy = (cb) => {\n    WIKI.server.close(cb)\n    for (let key in openConnections) {\n      openConnections[key].destroy()\n    }\n  }\n\n  WIKI.server.on('error', (error) => {\n    if (error.syscall !== 'listen') {\n      throw error\n    }\n\n    switch (error.code) {\n      case 'EACCES':\n        WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')\n        return process.exit(1)\n      case 'EADDRINUSE':\n        WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')\n        return process.exit(1)\n      default:\n        throw error\n    }\n  })\n\n  WIKI.server.on('listening', () => {\n    WIKI.logger.info('HTTP Server: [ RUNNING ]')\n    WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻')\n    WIKI.logger.info('')\n    WIKI.logger.info(`Browse to http://YOUR-SERVER-IP:${WIKI.config.port}/ to complete setup!`)\n    WIKI.logger.info('')\n    WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')\n  })\n}\n"
  },
  {
    "path": "server/templates/account-reset-pwd.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n    <meta charset=\"utf-8\"> <!-- utf-8 works for most cases -->\n    <meta name=\"viewport\" content=\"width=device-width\"> <!-- Forcing initial-scale shouldn't be necessary -->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <!-- Use the latest (edge) version of IE rendering engine -->\n    <meta name=\"x-apple-disable-message-reformatting\">  <!-- Disable auto-scale in iOS 10 Mail entirely -->\n    <title></title> <!-- The title tag shows in email notifications, like Android 4.4. -->\n\n    <!-- Web Font / @font-face : BEGIN -->\n    <!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->\n\n    <!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->\n    <!--[if mso]>\n        <style>\n            * {\n                font-family: sans-serif !important;\n            }\n        </style>\n    <![endif]-->\n\n    <!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->\n    <!--[if !mso]><!-->\n    <!-- insert web font reference, eg: <link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'> -->\n    <!--<![endif]-->\n\n    <!-- Web Font / @font-face : END -->\n\n    <!-- CSS Reset : BEGIN -->\n    <style>\n\n        /* What it does: Remove spaces around the email design added by some email clients. */\n        /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */\n        html,\n        body {\n            margin: 0 auto !important;\n            padding: 0 !important;\n            height: 100% !important;\n            width: 100% !important;\n        }\n\n        /* What it does: Stops email clients resizing small text. */\n        * {\n            -ms-text-size-adjust: 100%;\n            -webkit-text-size-adjust: 100%;\n        }\n\n        /* What it does: Centers email on Android 4.4 */\n        div[style*=\"margin: 16px 0\"] {\n            margin: 0 !important;\n        }\n\n        /* What it does: Stops Outlook from adding extra spacing to tables. */\n        table,\n        td {\n            mso-table-lspace: 0pt !important;\n            mso-table-rspace: 0pt !important;\n        }\n\n        /* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */\n        table {\n            border-spacing: 0 !important;\n            border-collapse: collapse !important;\n            table-layout: fixed !important;\n            margin: 0 auto !important;\n        }\n        table table table {\n            table-layout: auto;\n        }\n\n        /* What it does: Uses a better rendering method when resizing images in IE. */\n        img {\n            -ms-interpolation-mode:bicubic;\n        }\n\n        /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */\n        a {\n            text-decoration: none;\n        }\n\n        /* What it does: A work-around for email clients meddling in triggered links. */\n        *[x-apple-data-detectors],  /* iOS */\n        .unstyle-auto-detected-links *,\n        .aBn {\n            border-bottom: 0 !important;\n            cursor: default !important;\n            color: inherit !important;\n            text-decoration: none !important;\n            font-size: inherit !important;\n            font-family: inherit !important;\n            font-weight: inherit !important;\n            line-height: inherit !important;\n        }\n\n        /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */\n        .a6S {\n            display: none !important;\n            opacity: 0.01 !important;\n        }\n\n        /* What it does: Prevents Gmail from changing the text color in conversation threads. */\n        .im {\n            color: inherit !important;\n        }\n\n        /* If the above doesn't work, add a .g-img class to any image in question. */\n        img.g-img + div {\n            display: none !important;\n        }\n\n        /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89  */\n        /* Create one of these media queries for each additional viewport size you'd like to fix */\n\n        /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */\n        @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {\n            u ~ div .email-container {\n                min-width: 320px !important;\n            }\n        }\n        /* iPhone 6, 6S, 7, 8, and X */\n        @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {\n            u ~ div .email-container {\n                min-width: 375px !important;\n            }\n        }\n        /* iPhone 6+, 7+, and 8+ */\n        @media only screen and (min-device-width: 414px) {\n            u ~ div .email-container {\n                min-width: 414px !important;\n            }\n        }\n\n    </style>\n    <!-- CSS Reset : END -->\n\t<!-- Reset list spacing because Outlook ignores much of our inline CSS. -->\n\t<!--[if mso]>\n\t<style type=\"text/css\">\n\t\tul,\n\t\tol {\n\t\t\tmargin: 0 !important;\n\t\t}\n\t\tli {\n\t\t\tmargin-left: 30px !important;\n\t\t}\n\t\tli.list-item-first {\n\t\t\tmargin-top: 0 !important;\n\t\t}\n\t\tli.list-item-last {\n\t\t\tmargin-bottom: 10px !important;\n\t\t}\n\t</style>\n\t<![endif]-->\n\n    <!-- Progressive Enhancements : BEGIN -->\n    <style>\n\n\t    /* What it does: Hover styles for buttons */\n\t    .button-td,\n\t    .button-a {\n\t        transition: all 100ms ease-in;\n\t    }\n\t    .button-td-primary:hover,\n\t    .button-a-primary:hover {\n\t        background: #1976d2 !important;\n\t        border-color: #1976d2 !important;\n\t    }\n\n\t    /* Media Queries */\n\t    @media screen and (max-width: 600px) {\n\n\t        /* What it does: Adjust typography on small screens to improve readability */\n\t        .email-container p {\n\t            font-size: 17px !important;\n\t        }\n\n\t    }\n\n    </style>\n    <!-- Progressive Enhancements : END -->\n\n    <!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->\n    <!--[if gte mso 9]>\n    <xml>\n        <o:OfficeDocumentSettings>\n            <o:AllowPNG/>\n            <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n    </xml>\n    <![endif]-->\n\n</head>\n<!--\n\tThe email background color (#222222) is defined in three places:\n\t1. body tag: for most email clients\n\t2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr\n\t3. mso conditional: For Windows 10 Mail\n-->\n<body width=\"100%\" style=\"margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #EEE;\">\n\t<center style=\"width: 100%; background-color: #EEE;\">\n    <!--[if mso | IE]>\n    <table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"background-color: #222222;\">\n    <tr>\n    <td>\n    <![endif]-->\n\n        <!-- Visually Hidden Preheader Text : BEGIN -->\n        <div style=\"display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;\">\n          <%= preheadertext %>\n        </div>\n        <!-- Visually Hidden Preheader Text : END -->\n\n        <!-- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary. -->\n        <!-- Preview Text Spacing Hack : BEGIN -->\n        <div style=\"display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;\">\n\t        &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;\n        </div>\n        <!-- Preview Text Spacing Hack : END -->\n\n        <!--\n            Set the email width. Defined in two places:\n            1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.\n            2. MSO tags for Desktop Windows Outlook enforce a 600px width.\n        -->\n        <div style=\"max-width: 600px; margin: 0 auto;\" class=\"email-container\">\n            <!--[if mso]>\n            <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"600\">\n            <tr>\n            <td>\n            <![endif]-->\n\n\t        <!-- Email Body : BEGIN -->\n\t        <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"margin: 0 auto;\">\n\t\t        <!-- Email Header : BEGIN -->\n\t            <tr>\n\t                <td style=\"padding: 20px 0; text-align: center\">\n\t                    <img src=\"<%= logo %>\" height=\"50\" alt=\"<%= siteTitle %>\" border=\"0\" style=\"width: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;\">\n\t                </td>\n\t            </tr>\n\t\t        <!-- Email Header : END -->\n\n                <!-- Hero Image, Flush : BEGIN -->\n                <tr>\n                    <td style=\"background-color: #ffffff;\">\n                        <img src=\"https://static.requarks.io/email/email-cover-book.jpg\" width=\"600\" height=\"\" alt=\"<%= title %>\" border=\"0\" style=\"width: 100%; max-width: 600px; height: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555; margin: auto;\" class=\"g-img\">\n                    </td>\n                </tr>\n                <!-- Hero Image, Flush : END -->\n\n                <!-- 1 Column Text + Button : BEGIN -->\n                <tr>\n                    <td style=\"background-color: #ffffff;\">\n                        <table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\">\n                            <tr>\n                                <td style=\"padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;\">\n                                    <h1 style=\"margin: 0 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;\"><%= title %></h1>\n                                    <p style=\"margin: 0;\"><%= content %></p>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"padding: 0 20px 20px 20px;\">\n                                    <!-- Button : BEGIN -->\n                                    <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"margin: auto;\">\n                                        <tr>\n                                            <td class=\"button-td button-td-primary\" style=\"border-radius: 4px; background: #1976d2;\">\n                                                <a class=\"button-a button-a-primary\" href=\"<%= buttonLink %>\" style=\"background: #1976d2; border: 1px solid #1976d2; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;\"><%= buttonText %></a>\n                                            </td>\n                                        </tr>\n                                    </table>\n                                    <!-- Button : END -->\n                                </td>\n                            </tr>\n                        </table>\n                    </td>\n                </tr>\n                <!-- 1 Column Text + Button : END -->\n\n            </table>\n            <!-- Email Body : END -->\n\n            <!-- Email Footer : BEGIN -->\n\t        <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"margin: 0 auto;\">\n                <tr>\n                    <td style=\"padding: 20px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;\">\n                        <%= copyright %>\n                    </td>\n                </tr>\n            </table>\n            <!-- Email Footer : END -->\n\n            <!--[if mso]>\n            </td>\n            </tr>\n            </table>\n            <![endif]-->\n        </div>\n\n    <!--[if mso | IE]>\n    </td>\n    </tr>\n    </table>\n    <![endif]-->\n    </center>\n</body>\n</html>\n"
  },
  {
    "path": "server/templates/account-verify.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n    <meta charset=\"utf-8\"> <!-- utf-8 works for most cases -->\n    <meta name=\"viewport\" content=\"width=device-width\"> <!-- Forcing initial-scale shouldn't be necessary -->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <!-- Use the latest (edge) version of IE rendering engine -->\n    <meta name=\"x-apple-disable-message-reformatting\">  <!-- Disable auto-scale in iOS 10 Mail entirely -->\n    <title></title> <!-- The title tag shows in email notifications, like Android 4.4. -->\n\n    <!-- Web Font / @font-face : BEGIN -->\n    <!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->\n\n    <!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->\n    <!--[if mso]>\n        <style>\n            * {\n                font-family: sans-serif !important;\n            }\n        </style>\n    <![endif]-->\n\n    <!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->\n    <!--[if !mso]><!-->\n    <!-- insert web font reference, eg: <link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'> -->\n    <!--<![endif]-->\n\n    <!-- Web Font / @font-face : END -->\n\n    <!-- CSS Reset : BEGIN -->\n    <style>\n\n        /* What it does: Remove spaces around the email design added by some email clients. */\n        /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */\n        html,\n        body {\n            margin: 0 auto !important;\n            padding: 0 !important;\n            height: 100% !important;\n            width: 100% !important;\n        }\n\n        /* What it does: Stops email clients resizing small text. */\n        * {\n            -ms-text-size-adjust: 100%;\n            -webkit-text-size-adjust: 100%;\n        }\n\n        /* What it does: Centers email on Android 4.4 */\n        div[style*=\"margin: 16px 0\"] {\n            margin: 0 !important;\n        }\n\n        /* What it does: Stops Outlook from adding extra spacing to tables. */\n        table,\n        td {\n            mso-table-lspace: 0pt !important;\n            mso-table-rspace: 0pt !important;\n        }\n\n        /* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */\n        table {\n            border-spacing: 0 !important;\n            border-collapse: collapse !important;\n            table-layout: fixed !important;\n            margin: 0 auto !important;\n        }\n        table table table {\n            table-layout: auto;\n        }\n\n        /* What it does: Uses a better rendering method when resizing images in IE. */\n        img {\n            -ms-interpolation-mode:bicubic;\n        }\n\n        /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */\n        a {\n            text-decoration: none;\n        }\n\n        /* What it does: A work-around for email clients meddling in triggered links. */\n        *[x-apple-data-detectors],  /* iOS */\n        .unstyle-auto-detected-links *,\n        .aBn {\n            border-bottom: 0 !important;\n            cursor: default !important;\n            color: inherit !important;\n            text-decoration: none !important;\n            font-size: inherit !important;\n            font-family: inherit !important;\n            font-weight: inherit !important;\n            line-height: inherit !important;\n        }\n\n        /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */\n        .a6S {\n            display: none !important;\n            opacity: 0.01 !important;\n        }\n\n        /* What it does: Prevents Gmail from changing the text color in conversation threads. */\n        .im {\n            color: inherit !important;\n        }\n\n        /* If the above doesn't work, add a .g-img class to any image in question. */\n        img.g-img + div {\n            display: none !important;\n        }\n\n        /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89  */\n        /* Create one of these media queries for each additional viewport size you'd like to fix */\n\n        /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */\n        @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {\n            u ~ div .email-container {\n                min-width: 320px !important;\n            }\n        }\n        /* iPhone 6, 6S, 7, 8, and X */\n        @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {\n            u ~ div .email-container {\n                min-width: 375px !important;\n            }\n        }\n        /* iPhone 6+, 7+, and 8+ */\n        @media only screen and (min-device-width: 414px) {\n            u ~ div .email-container {\n                min-width: 414px !important;\n            }\n        }\n\n    </style>\n    <!-- CSS Reset : END -->\n\t<!-- Reset list spacing because Outlook ignores much of our inline CSS. -->\n\t<!--[if mso]>\n\t<style type=\"text/css\">\n\t\tul,\n\t\tol {\n\t\t\tmargin: 0 !important;\n\t\t}\n\t\tli {\n\t\t\tmargin-left: 30px !important;\n\t\t}\n\t\tli.list-item-first {\n\t\t\tmargin-top: 0 !important;\n\t\t}\n\t\tli.list-item-last {\n\t\t\tmargin-bottom: 10px !important;\n\t\t}\n\t</style>\n\t<![endif]-->\n\n    <!-- Progressive Enhancements : BEGIN -->\n    <style>\n\n\t    /* What it does: Hover styles for buttons */\n\t    .button-td,\n\t    .button-a {\n\t        transition: all 100ms ease-in;\n\t    }\n\t    .button-td-primary:hover,\n\t    .button-a-primary:hover {\n\t        background: #1976d2 !important;\n\t        border-color: #1976d2 !important;\n\t    }\n\n\t    /* Media Queries */\n\t    @media screen and (max-width: 600px) {\n\n\t        /* What it does: Adjust typography on small screens to improve readability */\n\t        .email-container p {\n\t            font-size: 17px !important;\n\t        }\n\n\t    }\n\n    </style>\n    <!-- Progressive Enhancements : END -->\n\n    <!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->\n    <!--[if gte mso 9]>\n    <xml>\n        <o:OfficeDocumentSettings>\n            <o:AllowPNG/>\n            <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n    </xml>\n    <![endif]-->\n\n</head>\n<!--\n\tThe email background color (#222222) is defined in three places:\n\t1. body tag: for most email clients\n\t2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr\n\t3. mso conditional: For Windows 10 Mail\n-->\n<body width=\"100%\" style=\"margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #EEE;\">\n\t<center style=\"width: 100%; background-color: #EEE;\">\n    <!--[if mso | IE]>\n    <table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"background-color: #222222;\">\n    <tr>\n    <td>\n    <![endif]-->\n\n        <!-- Visually Hidden Preheader Text : BEGIN -->\n        <div style=\"display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;\">\n          <%= preheadertext %>\n        </div>\n        <!-- Visually Hidden Preheader Text : END -->\n\n        <!-- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary. -->\n        <!-- Preview Text Spacing Hack : BEGIN -->\n        <div style=\"display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;\">\n\t        &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;\n        </div>\n        <!-- Preview Text Spacing Hack : END -->\n\n        <!--\n            Set the email width. Defined in two places:\n            1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.\n            2. MSO tags for Desktop Windows Outlook enforce a 600px width.\n        -->\n        <div style=\"max-width: 600px; margin: 0 auto;\" class=\"email-container\">\n            <!--[if mso]>\n            <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"600\">\n            <tr>\n            <td>\n            <![endif]-->\n\n\t        <!-- Email Body : BEGIN -->\n\t        <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"margin: 0 auto;\">\n\t\t        <!-- Email Header : BEGIN -->\n\t            <tr>\n\t                <td style=\"padding: 20px 0; text-align: center\">\n\t                    <img src=\"<%= logo %>\" height=\"50\" alt=\"<%= siteTitle %>\" border=\"0\" style=\"width: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;\">\n\t                </td>\n\t            </tr>\n\t\t        <!-- Email Header : END -->\n\n                <!-- Hero Image, Flush : BEGIN -->\n                <tr>\n                    <td style=\"background-color: #ffffff;\">\n                        <img src=\"https://static.requarks.io/email/email-cover-book.jpg\" width=\"600\" height=\"\" alt=\"<%= title %>\" border=\"0\" style=\"width: 100%; max-width: 600px; height: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555; margin: auto;\" class=\"g-img\">\n                    </td>\n                </tr>\n                <!-- Hero Image, Flush : END -->\n\n                <!-- 1 Column Text + Button : BEGIN -->\n                <tr>\n                    <td style=\"background-color: #ffffff;\">\n                        <table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\">\n                            <tr>\n                                <td style=\"padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;\">\n                                    <h1 style=\"margin: 0 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;\"><%= title %></h1>\n                                    <p style=\"margin: 0;\"><%= content %></p>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"padding: 0 20px 20px 20px;\">\n                                    <!-- Button : BEGIN -->\n                                    <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"margin: auto;\">\n                                        <tr>\n                                            <td class=\"button-td button-td-primary\" style=\"border-radius: 4px; background: #1976d2;\">\n                                                <a class=\"button-a button-a-primary\" href=\"<%= buttonLink %>\" style=\"background: #1976d2; border: 1px solid #1976d2; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;\"><%= buttonText %></a>\n                                            </td>\n                                        </tr>\n                                    </table>\n                                    <!-- Button : END -->\n                                </td>\n                            </tr>\n                        </table>\n                    </td>\n                </tr>\n                <!-- 1 Column Text + Button : END -->\n\n            </table>\n            <!-- Email Body : END -->\n\n            <!-- Email Footer : BEGIN -->\n\t        <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"margin: 0 auto;\">\n                <tr>\n                    <td style=\"padding: 20px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;\">\n                        <%= copyright %>\n                    </td>\n                </tr>\n            </table>\n            <!-- Email Footer : END -->\n\n            <!--[if mso]>\n            </td>\n            </tr>\n            </table>\n            <![endif]-->\n        </div>\n\n    <!--[if mso | IE]>\n    </td>\n    </tr>\n    </table>\n    <![endif]-->\n    </center>\n</body>\n</html>\n"
  },
  {
    "path": "server/templates/test.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n    <meta charset=\"utf-8\"> <!-- utf-8 works for most cases -->\n    <meta name=\"viewport\" content=\"width=device-width\"> <!-- Forcing initial-scale shouldn't be necessary -->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <!-- Use the latest (edge) version of IE rendering engine -->\n    <meta name=\"x-apple-disable-message-reformatting\">  <!-- Disable auto-scale in iOS 10 Mail entirely -->\n    <title></title> <!-- The title tag shows in email notifications, like Android 4.4. -->\n\n    <!-- Web Font / @font-face : BEGIN -->\n    <!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->\n\n    <!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->\n    <!--[if mso]>\n        <style>\n            * {\n                font-family: sans-serif !important;\n            }\n        </style>\n    <![endif]-->\n\n    <!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->\n    <!--[if !mso]><!-->\n    <!-- insert web font reference, eg: <link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'> -->\n    <!--<![endif]-->\n\n    <!-- Web Font / @font-face : END -->\n\n    <!-- CSS Reset : BEGIN -->\n    <style>\n\n        /* What it does: Remove spaces around the email design added by some email clients. */\n        /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */\n        html,\n        body {\n            margin: 0 auto !important;\n            padding: 0 !important;\n            height: 100% !important;\n            width: 100% !important;\n        }\n\n        /* What it does: Stops email clients resizing small text. */\n        * {\n            -ms-text-size-adjust: 100%;\n            -webkit-text-size-adjust: 100%;\n        }\n\n        /* What it does: Centers email on Android 4.4 */\n        div[style*=\"margin: 16px 0\"] {\n            margin: 0 !important;\n        }\n\n        /* What it does: Stops Outlook from adding extra spacing to tables. */\n        table,\n        td {\n            mso-table-lspace: 0pt !important;\n            mso-table-rspace: 0pt !important;\n        }\n\n        /* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */\n        table {\n            border-spacing: 0 !important;\n            border-collapse: collapse !important;\n            table-layout: fixed !important;\n            margin: 0 auto !important;\n        }\n        table table table {\n            table-layout: auto;\n        }\n\n        /* What it does: Uses a better rendering method when resizing images in IE. */\n        img {\n            -ms-interpolation-mode:bicubic;\n        }\n\n        /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */\n        a {\n            text-decoration: none;\n        }\n\n        /* What it does: A work-around for email clients meddling in triggered links. */\n        *[x-apple-data-detectors],  /* iOS */\n        .unstyle-auto-detected-links *,\n        .aBn {\n            border-bottom: 0 !important;\n            cursor: default !important;\n            color: inherit !important;\n            text-decoration: none !important;\n            font-size: inherit !important;\n            font-family: inherit !important;\n            font-weight: inherit !important;\n            line-height: inherit !important;\n        }\n\n        /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */\n        .a6S {\n            display: none !important;\n            opacity: 0.01 !important;\n        }\n\n        /* What it does: Prevents Gmail from changing the text color in conversation threads. */\n        .im {\n            color: inherit !important;\n        }\n\n        /* If the above doesn't work, add a .g-img class to any image in question. */\n        img.g-img + div {\n            display: none !important;\n        }\n\n        /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89  */\n        /* Create one of these media queries for each additional viewport size you'd like to fix */\n\n        /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */\n        @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {\n            u ~ div .email-container {\n                min-width: 320px !important;\n            }\n        }\n        /* iPhone 6, 6S, 7, 8, and X */\n        @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {\n            u ~ div .email-container {\n                min-width: 375px !important;\n            }\n        }\n        /* iPhone 6+, 7+, and 8+ */\n        @media only screen and (min-device-width: 414px) {\n            u ~ div .email-container {\n                min-width: 414px !important;\n            }\n        }\n\n    </style>\n    <!-- CSS Reset : END -->\n\t<!-- Reset list spacing because Outlook ignores much of our inline CSS. -->\n\t<!--[if mso]>\n\t<style type=\"text/css\">\n\t\tul,\n\t\tol {\n\t\t\tmargin: 0 !important;\n\t\t}\n\t\tli {\n\t\t\tmargin-left: 30px !important;\n\t\t}\n\t\tli.list-item-first {\n\t\t\tmargin-top: 0 !important;\n\t\t}\n\t\tli.list-item-last {\n\t\t\tmargin-bottom: 10px !important;\n\t\t}\n\t</style>\n\t<![endif]-->\n\n    <!-- Progressive Enhancements : BEGIN -->\n    <style>\n\n\t    /* What it does: Hover styles for buttons */\n\t    .button-td,\n\t    .button-a {\n\t        transition: all 100ms ease-in;\n\t    }\n\t    .button-td-primary:hover,\n\t    .button-a-primary:hover {\n\t        background: #1976d2 !important;\n\t        border-color: #1976d2 !important;\n\t    }\n\n\t    /* Media Queries */\n\t    @media screen and (max-width: 600px) {\n\n\t        /* What it does: Adjust typography on small screens to improve readability */\n\t        .email-container p {\n\t            font-size: 17px !important;\n\t        }\n\n\t    }\n\n    </style>\n    <!-- Progressive Enhancements : END -->\n\n    <!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->\n    <!--[if gte mso 9]>\n    <xml>\n        <o:OfficeDocumentSettings>\n            <o:AllowPNG/>\n            <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n    </xml>\n    <![endif]-->\n\n</head>\n<!--\n\tThe email background color (#222222) is defined in three places:\n\t1. body tag: for most email clients\n\t2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr\n\t3. mso conditional: For Windows 10 Mail\n-->\n<body width=\"100%\" style=\"margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #EEE;\">\n\t<center style=\"width: 100%; background-color: #EEE;\">\n    <!--[if mso | IE]>\n    <table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"background-color: #222222;\">\n    <tr>\n    <td>\n    <![endif]-->\n\n        <!-- Visually Hidden Preheader Text : BEGIN -->\n        <div style=\"display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;\">\n          <%= preheadertext %>\n        </div>\n        <!-- Visually Hidden Preheader Text : END -->\n\n        <!-- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary. -->\n        <!-- Preview Text Spacing Hack : BEGIN -->\n        <div style=\"display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;\">\n\t        &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;\n        </div>\n        <!-- Preview Text Spacing Hack : END -->\n\n        <!--\n            Set the email width. Defined in two places:\n            1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.\n            2. MSO tags for Desktop Windows Outlook enforce a 600px width.\n        -->\n        <div style=\"max-width: 600px; margin: 0 auto;\" class=\"email-container\">\n            <!--[if mso]>\n            <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"600\">\n            <tr>\n            <td>\n            <![endif]-->\n\n\t        <!-- Email Body : BEGIN -->\n\t        <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"margin: 0 auto;\">\n\t\t        <!-- Email Header : BEGIN -->\n\t            <tr>\n\t                <td style=\"padding: 20px 0; text-align: center\">\n\t                    <img src=\"<%= logo %>\" height=\"50\" alt=\"<%= siteTitle %>\" border=\"0\" style=\"width: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;\">\n\t                </td>\n\t            </tr>\n\t\t        <!-- Email Header : END -->\n\n                <!-- Hero Image, Flush : BEGIN -->\n                <tr>\n                    <td style=\"background-color: #ffffff;\">\n                        <img src=\"https://static.requarks.io/email/email-cover-book.jpg\" width=\"600\" height=\"\" alt=\"Test Email\" border=\"0\" style=\"width: 100%; max-width: 600px; height: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555; margin: auto;\" class=\"g-img\">\n                    </td>\n                </tr>\n                <!-- Hero Image, Flush : END -->\n\n                <!-- 1 Column Text + Button : BEGIN -->\n                <tr>\n                    <td style=\"background-color: #ffffff;\">\n                        <table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\">\n                            <tr>\n                                <td style=\"padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;\">\n                                    <h1 style=\"margin: 0 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;\">Hello there!</h1>\n                                    <p style=\"margin: 0;\">This is a test email sent from your wiki.</p>\n                                </td>\n                            </tr>\n                        </table>\n                    </td>\n                </tr>\n                <!-- 1 Column Text + Button : END -->\n\n            </table>\n            <!-- Email Body : END -->\n\n            <!-- Email Footer : BEGIN -->\n\t        <table align=\"center\" role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"margin: 0 auto;\">\n                <tr>\n                    <td style=\"padding: 20px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;\">\n                        <%= copyright %>\n                    </td>\n                </tr>\n            </table>\n            <!-- Email Footer : END -->\n\n            <!--[if mso]>\n            </td>\n            </tr>\n            </table>\n            <![endif]-->\n        </div>\n\n    <!--[if mso | IE]>\n    </td>\n    </tr>\n    </table>\n    <![endif]-->\n    </center>\n</body>\n</html>\n"
  },
  {
    "path": "server/test/helpers/page.test.js",
    "content": "const { injectPageMetadata } = require('../../helpers/page')\n\ndescribe('helpers/page/injectPageMetadata', () => {\n  const page = {\n    title: 'PAGE TITLE',\n    description: 'A PAGE',\n    isPublished: true,\n    updatedAt: new Date(),\n    content: 'TEST CONTENT',\n    createdAt: new Date('2019-01-01')\n  }\n\n  it('returns the page content by default when content type is unknown', () => {\n    const expected = 'TEST CONTENT'\n    const result = injectPageMetadata(page)\n    expect(result).toEqual(expected)\n  })\n\n  it('injects metadata for markdown contents', () => {\n    const markdownPage = {\n      ...page,\n      contentType: 'markdown',\n      editorKey: 'markdown'\n    }\n\n    const expected = `---\ntitle: ${markdownPage.title}\ndescription: ${markdownPage.description}\npublished: ${markdownPage.isPublished.toString()}\ndate: ${markdownPage.updatedAt}\ntags:\\x20\neditor: ${markdownPage.editorKey}\ndateCreated: ${markdownPage.createdAt}\\n---\n\nTEST CONTENT`\n\n    const result = injectPageMetadata(markdownPage)\n    expect(result).toEqual(expected)\n  })\n\n  it('injects metadata for html contents', () => {\n    const htmlPage = {\n      ...page,\n      contentType: 'html',\n      editorKey: 'html'\n    }\n\n    const expected = `<!--\ntitle: ${htmlPage.title}\ndescription: ${htmlPage.description}\npublished: ${htmlPage.isPublished.toString()}\ndate: ${htmlPage.updatedAt}\ntags:\\x20\neditor: ${htmlPage.editorKey}\ndateCreated: ${htmlPage.createdAt}\\n-->\n\nTEST CONTENT`\n\n    const result = injectPageMetadata(htmlPage)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "server/themes/default/theme.yml",
    "content": "name: Default\nauthor: requarks.io\nsite: https://wiki.requarks.io/\nversion: 1.0.0\nrequirements:\n  minimum: '>= 2.0.0'\n  maximum: '< 3.0.0'\nprops:\n  accentColor:\n    type: String\n    title: Accent Color\n    hint: Color used in the sidebar navigation and other elements.\n    order: 1\n    default: blue darken-2\n    control: color-material\n  tocPosition:\n    type: String\n    title: Table of Contents Position\n    hint: Select whether the table of contents is shown on the left, right or not at all.\n    order: 2\n    default: left\n    enum:\n      - left\n      - right\n      - hidden\n"
  },
  {
    "path": "server/views/admin.pug",
    "content": "extends master.pug\n\nblock body\n  #root\n    admin\n"
  },
  {
    "path": "server/views/editor.pug",
    "content": "extends master.pug\n\nblock head\n  if injectCode.css\n    style(type='text/css')!= injectCode.css\n\nblock body\n  #root\n    editor(\n      :page-id=page.id\n      locale=page.localeCode\n      path=page.path\n      title=page.title\n      description=page.description\n      :tags=page.tags\n      :is-published=page.isPublished\n      publish-start-date=page.publishStartDate\n      publish-end-date=page.publishEndDate\n      script-css=page.extra.css\n      script-js=page.extra.js\n      init-mode=page.mode\n      init-editor=page.editorKey\n      init-content=page.content\n      checkout-date=page.updatedAt\n      effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')\n      )\n"
  },
  {
    "path": "server/views/error.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    .app-error\n      a(href='/')\n        img(src='/_assets/svg/logo-wikijs.svg')\n      strong Oops, something went wrong...\n      span= message\n            \n      if error.stack\n        pre: code #{error.stack}\n"
  },
  {
    "path": "server/views/history.pug",
    "content": "extends master.pug\n\nblock head\n\nblock body\n  #root\n    history(\n      :page-id=page.id\n      locale=page.localeCode\n      path=page.path\n      title=page.title\n      description=page.description\n      :tags=page.tags\n      created-at=page.createdAt\n      updated-at=page.updatedAt\n      author-name=page.authorName\n      :author-id=page.authorId\n      :is-published=page.isPublished.toString()\n      live-content=page.content\n      effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')\n      )\n"
  },
  {
    "path": "server/views/legacy/login.pug",
    "content": "extends master.pug\n\nblock body\n  #root\n    .login-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '<a href=\"https://bestvpn.org/outdatedbrowser/en\" rel=\"nofollow\">' + t('modernBrowser') + '</a>', interpolation: { escapeValue: false } })\n    .login\n      .login-dialog\n        if err\n          .login-error= err.message\n        form(method='post', action='/login')\n          h1= config.title\n          select(name='strategy')\n            each str in formStrategies\n              option(value=str.key, selected)= str.title\n          input(type='text', name='user', placeholder=t('auth:fields.emailUser'))\n          input(type='password', name='pass', placeholder=t('auth:fields.password'))\n          button(type='submit')= t('auth:actions.login')\n        if socialStrategies.length\n          .login-social\n            h2= t('auth:orLoginUsingStrategy')\n            each str in socialStrategies\n              a.login-social-icon(href='/login/' + str.key, class=str.color)\n                != str.icon\n"
  },
  {
    "path": "server/views/legacy/page.pug",
    "content": "extends master.pug\n\nblock head\n  if injectCode.css\n    style(type='text/css')!= injectCode.css\n  if injectCode.head\n    != injectCode.head\n\nblock body\n  #root\n    .header\n      span.header-title= siteConfig.title\n      span.header-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '<a href=\"https://bestvpn.org/outdatedbrowser/en\" rel=\"nofollow\">' + t('modernBrowser') + '</a>', interpolation: { escapeValue: false } })\n      span.header-login\n        if !isAuthenticated\n          a(href='/login', title='Login')\n            i.mdi.mdi-account-circle\n        else\n          a(href='/logout', title='Logout')\n            i.mdi.mdi-logout\n    .main\n      .sidebar\n        each navItem in sidebar\n          if navItem.k === 'link'\n            a.sidebar-link(href=navItem.t)\n              i.mdi(class=navItem.c)\n              span= navItem.l\n          else if navItem.k === 'divider'\n            .sidebar-divider\n          else if navItem.k === 'header'\n            .sidebar-title= navItem.l\n      .main-container\n        .page-header\n          .page-header-left\n            h1= page.title\n            h2= page.description\n          //- .page-header-right\n          //-   .page-header-right-title Last edited by\n          //-   .page-header-right-author= page.authorName\n          //-   .page-header-right-updated= page.updatedAt\n        .page-contents.v-content\n          .contents\n            div!= page.render\n          if page.toc.length\n            .toc\n              .toc-title= t('page.toc')\n              each tocItem, tocIdx in page.toc\n                a.toc-tile(href=tocItem.anchor)\n                  i.mdi.mdi-chevron-right\n                  span= tocItem.title\n                if tocIdx < page.toc.length - 1 || tocItem.children.length\n                  .toc-divider\n                each tocSubItem in tocItem.children\n                  a.toc-tile.inset(href=tocSubItem.anchor)\n                    i.mdi.mdi-chevron-right\n                    span= tocSubItem.title\n                  if tocIdx < page.toc.length - 1\n                    .toc-divider.inset\n  if injectCode.body\n    != injectCode.body\n"
  },
  {
    "path": "server/views/login.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    login(\n      bg-url=bgUrl\n      hide-local=hideLocal\n      change-pwd-continuation-token=changePwdContinuationToken\n    )\n"
  },
  {
    "path": "server/views/new.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    new-page(locale=locale, path=path)\n"
  },
  {
    "path": "server/views/notfound.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    not-found\n"
  },
  {
    "path": "server/views/page.pug",
    "content": "extends master.pug\n\nblock head\n  if injectCode.css\n    style(type='text/css')!= injectCode.css\n  if injectCode.head\n    != injectCode.head\n  if config.features.featurePageComments\n    != comments.head\n\nblock body\n  #root\n    page(\n      locale=page.localeCode\n      path=page.path\n      title=page.title\n      description=page.description\n      :tags=page.tags\n      created-at=page.createdAt\n      updated-at=page.updatedAt\n      author-name=page.authorName\n      :author-id=page.authorId\n      editor=page.editorKey\n      :is-published=page.isPublished.toString()\n      toc=Buffer.from(page.toc).toString('base64')\n      :page-id=page.id\n      sidebar=Buffer.from(JSON.stringify(sidebar)).toString('base64')\n      nav-mode=config.nav.mode\n      comments-enabled=config.features.featurePageComments\n      effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')\n      comments-external=comments.codeTemplate\n      edit-shortcuts=Buffer.from(JSON.stringify(config.editShortcuts)).toString('base64')\n      filename=pageFilename\n      )\n      template(slot='contents')\n        div!= page.render\n      template(slot='comments')\n        div!= comments.main\n  if injectCode.body\n    != injectCode.body\n  if config.features.featurePageComments\n    != comments.body\n"
  },
  {
    "path": "server/views/profile.pug",
    "content": "extends master.pug\n\nblock body\n  #root\n    profile\n"
  },
  {
    "path": "server/views/register.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    register\n"
  },
  {
    "path": "server/views/source.pug",
    "content": "extends master.pug\n\nblock head\n\nblock body\n  #root\n    page-source(\n      :page-id=page.id\n      locale=page.localeCode\n      path=page.path\n      :version-id=page.versionId\n      version-date=page.versionDate\n      effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')\n      )\n      code(v-pre)= page.content\n"
  },
  {
    "path": "server/views/tags.pug",
    "content": "extends master.pug\n\nblock body\n  #root\n    tags\n"
  },
  {
    "path": "server/views/unauthorized.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    unauthorized(action=action)\n"
  },
  {
    "path": "server/views/welcome.pug",
    "content": "extends master.pug\n\nblock body\n  #root.is-fullscreen\n    welcome(locale=locale)\n"
  }
]